diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..8f60a9b
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,3 @@
+# RushDB Configuration
+RUSHDB_TOKEN=your_api_token_here
+RUSHDB_URL=http://localhost:3000
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..57ed4d5
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,73 @@
+name: Python SDK CI/CD
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+ release:
+ types: [published]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.8", "3.9", "3.10", "3.11"]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install Poetry
+ uses: snok/install-poetry@v1
+ with:
+ version: 1.7.1
+ virtualenvs-create: true
+ virtualenvs-in-project: true
+
+ - name: Install dependencies
+ run: |
+ poetry install --no-interaction --no-root
+
+ - name: Run linters
+ run: |
+ poetry run black . --check
+ poetry run isort . --check
+ poetry run ruff check .
+ poetry run mypy src/rushdb
+
+ publish:
+ if: startsWith(github.ref, 'refs/tags/v')
+ runs-on: ubuntu-latest
+ needs: lint
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.11"
+
+ - name: Install Poetry
+ uses: snok/install-poetry@v1
+ with:
+ version: 1.7.1
+ virtualenvs-create: true
+ virtualenvs-in-project: true
+
+ - name: Install dependencies
+ run: |
+ poetry install --no-interaction --no-root
+
+ - name: Build and publish
+ env:
+ POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
+ run: |
+ poetry build
+ poetry publish
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..05bbe98
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+node_modules
+dist
+*.log
+*.tgz
+.env
+.next
+.DS_Store
+.idea
+.vscode
+.eslintcache
+examples/**/yarn.lock
+package-lock.json
+*.tsbuildinfo
+coverage
+.rollup.cache
+cjs
+esm
+packages/javascript-sdk/types
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..6480fb8
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,56 @@
+# Code of Conduct
+
+We value open collaboration and respect in all interactions. To foster a welcoming and productive community, all participants are expected to adhere to the following guidelines.
+
+## Scope
+
+This Code of Conduct applies to all contributors, including maintainers, users, and collaborators, in all project spaces and public communication channels.
+
+## Our Standards
+
+1. **Respectful Communication**
+ - Use welcoming and inclusive language.
+ - Be respectful of differing viewpoints and experiences.
+ - Refrain from personal attacks or derogatory comments.
+
+2. **Collaboration**
+ - Provide constructive feedback and suggestions.
+ - Share knowledge and help others grow within the community.
+
+3. **Responsibility**
+ - Take responsibility for your actions and their impact on others.
+ - Report issues or concerns to maintainers or moderators.
+
+4. **Inclusivity**
+ - Actively seek to include and empower underrepresented groups.
+ - Avoid biased or discriminatory behavior.
+
+## Unacceptable Behavior
+
+Examples of unacceptable behavior include:
+
+- Harassment, bullying, or intimidation.
+- Disrespectful, offensive, or inappropriate comments.
+- Discriminatory jokes or language.
+- Publishing private information without explicit permission.
+
+## Reporting Violations
+
+If you observe or experience behavior that violates this Code of Conduct, please report it by:
+
+- Contacting the project maintainer: [tg:onepx](https://t.me/onepx)
+- Messaging via LinkedIn: [linkedin.com/onepx](https://linkedin.com/in/onepx)
+
+We take all reports seriously and will investigate and address them promptly.
+
+## Enforcement
+
+Participants found to be in violation of this Code of Conduct may face actions such as:
+
+- A private warning or reprimand.
+- Temporary or permanent ban from project spaces.
+- Removal of contributions or privileges.
+
+## Acknowledgments
+
+This Code of Conduct is adapted from widely recognized community guidelines to reflect our commitment to a healthy and collaborative environment.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..61802e4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,43 @@
+# Contribution Guidelines for RushDB
+
+Thank you for your interest in contributing to RushDB! To ensure a smooth contribution process, please follow the checklist below when reporting issues or submitting changes.
+
+## Reporting Issues
+
+When reporting an issue, include the following information:
+
+1. **Minimum Reproducible Data Set**
+ - Provide a small JSON or CSV dataset if the issue is related to the core, dashboard, or SDK.
+ - Ensure the dataset highlights the problem clearly.
+
+2. **RushDB Version**
+ - Specify the version of RushDB you are using:
+ - **Cloud**: Mention if you are using the latest cloud version.
+ - **Self-hosted**: Provide the tag from Docker Hub or the SDK version.
+
+3. **Steps to Reproduce**
+ - Give a detailed explanation of how to reproduce the issue.
+ - Include any configurations, commands, or environment settings.
+
+4. **Query Examples**
+ - If applicable, include specific queries that trigger the error.
+
+5. **Minimum Repository (if SDK-related)**
+ - For issues related to the SDK, a minimal GitHub repository demonstrating the bug may be required.
+
+## Submitting Changes
+
+Before submitting a pull request:
+
+- Ensure your code adheres to the project's coding standards.
+- Include unit tests for new functionality or bug fixes.
+- Update documentation if necessary.
+
+## Contact Information
+
+For urgent issues or further assistance, you can reach out directly:
+
+- **Telegram**: [tg:onepx](https://t.me/onepx)
+- **LinkedIn**: [linkedin.com/onepx](https://linkedin.com/in/onepx)
+
+We appreciate your contributions and look forward to your feedback!
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3350362
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2025 Collect Software Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/README.md b/README.md
index 35b0bc5..7e827a8 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,150 @@
-# rushdb-python
-RushDB Python SDK
+
+
+
+
+# RushDB Python SDK
+### The Instant Database for Modern Apps and DS/ML Ops
+
+RushDB is an open-source database built on Neo4j, designed to simplify application development.
+
+It automates data normalization, manages relationships, and infers data types, enabling developers to focus on building features rather than wrestling with data.
+
+[🌐 Homepage](https://rushdb.com) — [📢 Blog](https://rushdb.com/blog) — [☁️ Platform ](https://app.rushdb.com) — [📚 Docs](https://docs.rushdb.com) — [🧑💻 Examples](https://github.com/rush-db/rushdb/examples)
+
+
+## 🚀 Feature Highlights
+
+### 1. **Data modeling is optional**
+Push data of any shape—RushDB handles relationships, data types, and more automatically.
+
+### 2. **Automatic type inference**
+Minimizes overhead while optimizing performance for high-speed searches.
+
+### 3. **Powerful search API**
+Query data with accuracy using the graph-powered search API.
+
+### 4. **Flexible data import**
+Easily import data in `JSON`, `CSV`, or `JSONB`, creating data-rich applications fast.
+
+### 5. **Developer-Centric Design**
+RushDB prioritizes DX with an intuitive and consistent API.
+
+### 6. **REST API Readiness**
+A REST API with SDK-like DX for every operation: manage relationships, create, delete, and search effortlessly. Same DTO everywhere.
+
+---
+
+## Installation
+
+Install the RushDB Python SDK via pip:
+
+```sh
+pip install rushdb
+```
+
+---
+
+## Usage
+
+### **1. Setup SDK**
+
+```python
+from rushdb import RushDB
+
+db = RushDB("API_TOKEN", url="https://api.rushdb.com")
+```
+
+---
+
+### **2. Push any JSON data**
+
+```python
+company_data = {
+ "label": "COMPANY",
+ "payload": {
+ "name": "Google LLC",
+ "address": "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA",
+ "foundedAt": "1998-09-04T00:00:00.000Z",
+ "rating": 4.9,
+ "DEPARTMENT": [{
+ "name": "Research & Development",
+ "description": "Innovating and creating advanced technologies for AI, cloud computing, and consumer devices.",
+ "PROJECT": [{
+ "name": "Bard AI",
+ "description": "A state-of-the-art generative AI model for natural language understanding and creation.",
+ "active": True,
+ "budget": 1200000000,
+ "EMPLOYEE": [{
+ "name": "Jeff Dean",
+ "position": "Head of AI Research",
+ "email": "jeff@google.com",
+ "dob": "1968-07-16T00:00:00.000Z",
+ "salary": 3000000
+ }]
+ }]
+ }]
+ }
+}
+
+db.records.create_many(company_data)
+```
+
+---
+
+### **3. Find Records by specific criteria**
+
+```python
+query = {
+ "labels": ["EMPLOYEE"],
+ "where": {
+ "position": {"$contains": "AI"},
+ "PROJECT": {
+ "DEPARTMENT": {
+ "COMPANY": {
+ "rating": {"$gte": 4}
+ }
+ }
+ }
+ }
+}
+
+matched_employees = db.records.find(query)
+
+company = db.records.find_uniq("COMPANY", {"where": {"name": "Google LLC"}})
+```
+
+---
+
+### **4. Use REST API with cURL**
+
+```sh
+curl -X POST https://api.rushdb.com/api/v1/records/search \
+-H "Authorization: Bearer API_TOKEN" \
+-H "Content-Type: application/json" \
+-d '{
+ "labels": ["EMPLOYEE"],
+ "where": {
+ "position": { "$contains": "AI" },
+ "PROJECT": {
+ "DEPARTMENT": {
+ "COMPANY": {
+ "rating": { "$gte": 4 }
+ }
+ }
+ }
+ }
+}'
+```
+
+
+You Rock 🚀
+
+
+---
+
+
+
+> Check the [Documentation](https://docs.rushdb.com) and [Examples](https://github.com/rush-db/rushdb/examples) to learn more 🧐
+
+
+
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..1dada32
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,676 @@
+# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
+
+[[package]]
+name = "black"
+version = "23.12.1"
+description = "The uncompromising code formatter."
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"},
+ {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"},
+ {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"},
+ {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"},
+ {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"},
+ {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"},
+ {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"},
+ {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"},
+ {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"},
+ {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"},
+ {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"},
+ {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"},
+ {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"},
+ {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"},
+ {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"},
+ {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"},
+ {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"},
+ {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"},
+ {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"},
+ {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"},
+ {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"},
+ {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"},
+]
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+packaging = ">=22.0"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "certifi"
+version = "2025.1.31"
+description = "Python package for providing Mozilla's CA Bundle."
+optional = false
+python-versions = ">=3.6"
+groups = ["main"]
+files = [
+ {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
+ {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.1"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"},
+ {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"},
+ {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"},
+ {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"},
+ {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"},
+ {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"},
+ {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"},
+ {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"},
+ {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"},
+ {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"},
+ {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"},
+ {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"},
+ {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"},
+ {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"},
+ {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"},
+ {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"},
+ {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"},
+ {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"},
+ {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"},
+ {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"},
+ {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"},
+ {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"},
+ {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"},
+ {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"},
+ {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"},
+ {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"},
+ {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"},
+ {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"},
+ {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"},
+ {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"},
+ {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"},
+ {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"},
+ {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"},
+ {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"},
+ {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"},
+ {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"},
+ {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"},
+ {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"},
+ {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"},
+ {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"},
+ {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"},
+ {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"},
+ {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"},
+ {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"},
+ {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"},
+ {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"},
+ {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"},
+ {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"},
+ {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"},
+ {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"},
+ {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"},
+ {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"},
+ {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"},
+ {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"},
+ {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"},
+ {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"},
+ {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"},
+ {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"},
+ {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"},
+ {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"},
+ {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"},
+ {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"},
+ {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"},
+ {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"},
+ {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"},
+ {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"},
+ {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"},
+ {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"},
+ {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"},
+ {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"},
+ {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"},
+ {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"},
+ {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"},
+ {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"},
+ {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"},
+ {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"},
+ {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"},
+ {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"},
+ {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"},
+ {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"},
+ {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"},
+ {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"},
+ {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"},
+ {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"},
+ {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"},
+ {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"},
+ {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"},
+ {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"},
+ {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"},
+ {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"},
+ {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"},
+ {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"},
+]
+
+[[package]]
+name = "click"
+version = "8.1.8"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+files = [
+ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
+ {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["dev"]
+markers = "platform_system == \"Windows\" or sys_platform == \"win32\""
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "coverage"
+version = "7.6.1"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"},
+ {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"},
+ {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"},
+ {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"},
+ {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"},
+ {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"},
+ {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"},
+ {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"},
+ {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"},
+ {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"},
+ {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"},
+ {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"},
+ {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"},
+ {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"},
+ {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"},
+ {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"},
+ {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"},
+ {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"},
+ {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"},
+ {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"},
+ {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"},
+ {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"},
+ {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"},
+ {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"},
+ {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"},
+ {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"},
+ {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"},
+ {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"},
+ {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"},
+ {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"},
+ {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"},
+ {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"},
+ {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"},
+ {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"},
+ {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"},
+ {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"},
+ {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"},
+ {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"},
+ {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"},
+ {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"},
+ {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"},
+ {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"},
+ {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"},
+ {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"},
+ {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"},
+ {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"},
+ {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"},
+ {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"},
+ {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"},
+ {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"},
+ {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"},
+ {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"},
+ {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"},
+ {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"},
+ {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"},
+ {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"},
+ {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"},
+ {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"},
+ {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"},
+ {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"},
+ {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"},
+ {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"},
+ {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"},
+ {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"},
+ {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"},
+ {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"},
+ {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"},
+ {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"},
+ {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"},
+ {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"},
+ {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"},
+ {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"},
+]
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.2"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+markers = "python_version < \"3.11\""
+files = [
+ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
+ {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "idna"
+version = "3.10"
+description = "Internationalized Domain Names in Applications (IDNA)"
+optional = false
+python-versions = ">=3.6"
+groups = ["main"]
+files = [
+ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
+ {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
+]
+
+[package.extras]
+all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+files = [
+ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+
+[[package]]
+name = "isort"
+version = "5.13.2"
+description = "A Python utility / library to sort Python imports."
+optional = false
+python-versions = ">=3.8.0"
+groups = ["dev"]
+files = [
+ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
+ {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
+]
+
+[package.extras]
+colors = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "mypy"
+version = "1.14.1"
+description = "Optional static typing for Python"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"},
+ {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"},
+ {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"},
+ {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"},
+ {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"},
+ {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"},
+ {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"},
+ {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"},
+ {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"},
+ {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"},
+ {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"},
+ {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"},
+ {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"},
+ {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"},
+ {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"},
+ {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"},
+ {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"},
+ {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"},
+ {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"},
+ {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"},
+ {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"},
+ {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"},
+ {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"},
+ {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"},
+ {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"},
+ {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"},
+ {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"},
+ {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"},
+ {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"},
+ {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"},
+ {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"},
+ {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"},
+ {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"},
+ {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"},
+ {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"},
+ {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"},
+ {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"},
+ {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"},
+]
+
+[package.dependencies]
+mypy_extensions = ">=1.0.0"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing_extensions = ">=4.6.0"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+faster-cache = ["orjson"]
+install-types = ["pip"]
+mypyc = ["setuptools (>=50)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+optional = false
+python-versions = ">=3.5"
+groups = ["dev"]
+files = [
+ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+]
+
+[[package]]
+name = "packaging"
+version = "24.2"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
+ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
+ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.6"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
+ {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
+]
+
+[package.extras]
+docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
+type = ["mypy (>=1.11.2)"]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+ {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pytest"
+version = "7.4.4"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+files = [
+ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
+ {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-cov"
+version = "4.1.0"
+description = "Pytest plugin for measuring coverage."
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+files = [
+ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
+ {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
+]
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+
+[[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"
+groups = ["main"]
+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 = "requests"
+version = "2.32.3"
+description = "Python HTTP for Humans."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
+ {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
+]
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<4"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<3"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "ruff"
+version = "0.0.280"
+description = "An extremely fast Python linter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+files = [
+ {file = "ruff-0.0.280-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:48ed5aca381050a4e2f6d232db912d2e4e98e61648b513c350990c351125aaec"},
+ {file = "ruff-0.0.280-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:ef6ee3e429fd29d6a5ceed295809e376e6ece5b0f13c7e703efaf3d3bcb30b96"},
+ {file = "ruff-0.0.280-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d878370f7e9463ac40c253724229314ff6ebe4508cdb96cb536e1af4d5a9cd4f"},
+ {file = "ruff-0.0.280-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83e8f372fa5627eeda5b83b5a9632d2f9c88fc6d78cead7e2a1f6fb05728d137"},
+ {file = "ruff-0.0.280-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7008fc6ca1df18b21fa98bdcfc711dad5f94d0fc3c11791f65e460c48ef27c82"},
+ {file = "ruff-0.0.280-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fe7118c1eae3fda17ceb409629c7f3b5a22dffa7caf1f6796776936dca1fe653"},
+ {file = "ruff-0.0.280-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37359cd67d2af8e09110a546507c302cbea11c66a52d2a9b6d841d465f9962d4"},
+ {file = "ruff-0.0.280-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd58af46b0221efb95966f1f0f7576df711cb53e50d2fdb0e83c2f33360116a4"},
+ {file = "ruff-0.0.280-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e7c15828d09f90e97bea8feefcd2907e8c8ce3a1f959c99f9b4b3469679f33c"},
+ {file = "ruff-0.0.280-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2dae8f2d9c44c5c49af01733c2f7956f808db682a4193180dedb29dd718d7bbe"},
+ {file = "ruff-0.0.280-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5f972567163a20fb8c2d6afc60c2ea5ef8b68d69505760a8bd0377de8984b4f6"},
+ {file = "ruff-0.0.280-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ffa7347ad11643f29de100977c055e47c988cd6d9f5f5ff83027600b11b9189"},
+ {file = "ruff-0.0.280-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37dab70114671d273f203268f6c3366c035fe0c8056614069e90a65e614bfc"},
+ {file = "ruff-0.0.280-py3-none-win32.whl", hash = "sha256:7784e3606352fcfb193f3cd22b2e2117c444cb879ef6609ec69deabd662b0763"},
+ {file = "ruff-0.0.280-py3-none-win_amd64.whl", hash = "sha256:4a7d52457b5dfcd3ab24b0b38eefaead8e2dca62b4fbf10de4cd0938cf20ce30"},
+ {file = "ruff-0.0.280-py3-none-win_arm64.whl", hash = "sha256:b7de5b8689575918e130e4384ed9f539ce91d067c0a332aedef6ca7188adac2d"},
+ {file = "ruff-0.0.280.tar.gz", hash = "sha256:581c43e4ac5e5a7117ad7da2120d960a4a99e68ec4021ec3cd47fe1cf78f8380"},
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+markers = "python_full_version <= \"3.11.0a6\""
+files = [
+ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
+ {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
+ {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
+ {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
+ {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
+ {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
+ {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
+ {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
+ {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
+ {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
+ {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
+ {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
+ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
+ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
+]
+
+[[package]]
+name = "types-python-dateutil"
+version = "2.9.0.20241206"
+description = "Typing stubs for python-dateutil"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"},
+ {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"},
+]
+
+[[package]]
+name = "types-requests"
+version = "2.32.0.20241016"
+description = "Typing stubs for requests"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"},
+ {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"},
+]
+
+[package.dependencies]
+urllib3 = ">=2"
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+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 = "urllib3"
+version = "2.2.3"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+optional = false
+python-versions = ">=3.8"
+groups = ["main", "dev"]
+files = [
+ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
+ {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
+]
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+h2 = ["h2 (>=4,<5)"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["zstandard (>=0.18.0)"]
+
+[metadata]
+lock-version = "2.1"
+python-versions = "^3.8"
+content-hash = "6fde1cdc1a65c51855c8926e27d767aba92a21138783bd7bd0f6398201731c5a"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..a6f0864
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,69 @@
+[tool.poetry]
+name = "rushdb"
+version = "0.1.0"
+description = "RushDB Python SDK"
+authors = ["RushDB Team "]
+license = "Apache-2.0"
+readme = "README.md"
+homepage = "https://github.com/rushdb/rushdb-python"
+repository = "https://github.com/rushdb/rushdb-python"
+documentation = "https://docs.rushdb.com"
+packages = [{ include = "rushdb", from = "src" }]
+keywords = [
+ "database",
+ "graph database",
+ "instant database",
+ "instant-database",
+ "instantdatabase",
+ "instant db",
+ "instant-db",
+ "instantdb",
+ "neo4j",
+ "cypher",
+ "ai",
+ "ai database",
+ "etl",
+ "data-pipeline",
+ "data science",
+ "data-science",
+ "data management",
+ "data-management",
+ "machine learning",
+ "machine-learning",
+ "persistence",
+ "db",
+ "graph",
+ "graphs",
+ "graph-database",
+ "self-hosted",
+ "rush-db",
+ "rush db",
+ "rushdb"
+]
+
+[tool.poetry.dependencies]
+python = "^3.8"
+python-dotenv = "^1.0.0"
+requests = "^2.31.0"
+
+[tool.poetry.dev-dependencies]
+black = "^23.7.0"
+isort = "^5.12.0"
+ruff = "^0.0.280"
+mypy = "^1.4.1"
+pytest = "^7.4.0"
+pytest-cov = "^4.1.0"
+types-requests = "^2.31.0.1"
+types-python-dateutil = "^2.8.19.14"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.ruff]
+line-length = 120
+
+[tool.isort]
+profile = "black"
+multi_line_output = 3
+line_length = 120
\ No newline at end of file
diff --git a/rushdb-logo.svg b/rushdb-logo.svg
new file mode 100644
index 0000000..f48558a
--- /dev/null
+++ b/rushdb-logo.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/rushdb/__init__.py b/src/rushdb/__init__.py
new file mode 100644
index 0000000..d6a8add
--- /dev/null
+++ b/src/rushdb/__init__.py
@@ -0,0 +1,21 @@
+"""RushDB Client Package
+
+Exposes the RushDBClient class.
+"""
+
+from .client import RushDBClient
+from .common import RushDBError
+from .models.property import Property
+from .models.record import Record
+from .models.relationship import RelationshipDetachOptions, RelationshipOptions
+from .models.transaction import Transaction
+
+__all__ = [
+ "RushDBClient",
+ "RushDBError",
+ "Record",
+ "Transaction",
+ "Property",
+ "RelationshipOptions",
+ "RelationshipDetachOptions",
+]
diff --git a/src/rushdb/api/__init__.py b/src/rushdb/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/rushdb/api/base.py b/src/rushdb/api/base.py
new file mode 100644
index 0000000..1632036
--- /dev/null
+++ b/src/rushdb/api/base.py
@@ -0,0 +1,11 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from ..client import RushDBClient
+
+
+class BaseAPI:
+ """Base class for all API endpoints."""
+
+ def __init__(self, client: "RushDBClient"):
+ self.client = client
diff --git a/src/rushdb/api/labels.py b/src/rushdb/api/labels.py
new file mode 100644
index 0000000..4710a61
--- /dev/null
+++ b/src/rushdb/api/labels.py
@@ -0,0 +1,25 @@
+import typing
+from typing import List, Optional
+
+from ..models.search_query import SearchQuery
+from ..models.transaction import Transaction
+from .base import BaseAPI
+
+
+class LabelsAPI(BaseAPI):
+ """API for managing labels in RushDB."""
+
+ def list(
+ self,
+ query: Optional[SearchQuery] = None,
+ transaction: Optional[Transaction] = None,
+ ) -> List[str]:
+ """List all labels."""
+ headers = Transaction._build_transaction_header(transaction)
+
+ return self.client._make_request(
+ "POST",
+ "/api/v1/labels",
+ data=typing.cast(typing.Dict[str, typing.Any], query or {}),
+ headers=headers,
+ )
diff --git a/src/rushdb/api/properties.py b/src/rushdb/api/properties.py
new file mode 100644
index 0000000..12345c3
--- /dev/null
+++ b/src/rushdb/api/properties.py
@@ -0,0 +1,64 @@
+import typing
+from typing import List, Literal, Optional
+
+from ..models.property import Property, PropertyValuesData
+from ..models.search_query import SearchQuery
+from ..models.transaction import Transaction
+from .base import BaseAPI
+
+
+class PropertiesAPI(BaseAPI):
+ """API for managing properties in RushDB."""
+
+ def find(
+ self,
+ query: Optional[SearchQuery] = None,
+ transaction: Optional[Transaction] = None,
+ ) -> List[Property]:
+ """List all properties."""
+ headers = Transaction._build_transaction_header(transaction)
+
+ return self.client._make_request(
+ "POST",
+ "/api/v1/properties",
+ typing.cast(typing.Dict[str, typing.Any], query or {}),
+ headers,
+ )
+
+ def find_by_id(
+ self, property_id: str, transaction: Optional[Transaction] = None
+ ) -> Property:
+ """Get a property by ID."""
+ headers = Transaction._build_transaction_header(transaction)
+
+ return self.client._make_request(
+ "GET", f"/api/v1/properties/{property_id}", headers=headers
+ )
+
+ def delete(
+ self, property_id: str, transaction: Optional[Transaction] = None
+ ) -> None:
+ """Delete a property."""
+ headers = Transaction._build_transaction_header(transaction)
+
+ return self.client._make_request(
+ "DELETE", f"/api/v1/properties/{property_id}", headers=headers
+ )
+
+ def values(
+ self,
+ property_id: str,
+ sort: Optional[Literal["asc", "desc"]],
+ skip: Optional[int],
+ limit: Optional[int],
+ transaction: Optional[Transaction] = None,
+ ) -> PropertyValuesData:
+ """Get values data for a property."""
+ headers = Transaction._build_transaction_header(transaction)
+
+ return self.client._make_request(
+ "GET",
+ f"/api/v1/properties/{property_id}/values",
+ headers=headers,
+ params={"sort": sort, "skip": skip, "limit": limit},
+ )
diff --git a/src/rushdb/api/records.py b/src/rushdb/api/records.py
new file mode 100644
index 0000000..5ba5c4d
--- /dev/null
+++ b/src/rushdb/api/records.py
@@ -0,0 +1,250 @@
+import typing
+from typing import Any, Dict, List, Optional, Union
+
+from ..models.record import Record
+from ..models.relationship import RelationshipDetachOptions, RelationshipOptions
+from ..models.search_query import SearchQuery
+from ..models.transaction import Transaction
+from .base import BaseAPI
+
+
+class RecordsAPI(BaseAPI):
+ """API for managing records in RushDB."""
+
+ def set(
+ self,
+ record_id: str,
+ data: Dict[str, Any],
+ transaction: Optional[Transaction] = None,
+ ) -> Dict[str, str]:
+ """Update a record by ID."""
+ headers = Transaction._build_transaction_header(transaction)
+ return self.client._make_request(
+ "PUT", f"/api/v1/records/{record_id}", data, headers
+ )
+
+ def update(
+ self,
+ record_id: str,
+ data: Dict[str, Any],
+ transaction: Optional[Transaction] = None,
+ ) -> Dict[str, str]:
+ """Update a record by ID."""
+ headers = Transaction._build_transaction_header(transaction)
+
+ return self.client._make_request(
+ "PATCH", f"/api/v1/records/{record_id}", data, headers
+ )
+
+ def create(
+ self,
+ label: str,
+ data: Dict[str, Any],
+ options: Optional[Dict[str, bool]] = None,
+ transaction: Optional[Transaction] = None,
+ ) -> Record:
+ """Create a new record.
+
+ Args:
+ label: Label for the record
+ data: Record data
+ options: Optional parsing and response options (returnResult, suggestTypes)
+ transaction: Optional transaction object
+
+ Returns:
+ Record object
+ :param
+ """
+ headers = Transaction._build_transaction_header(transaction)
+
+ payload = {
+ "label": label,
+ "payload": data,
+ "options": options or {"returnResult": True, "suggestTypes": True},
+ }
+ response = self.client._make_request(
+ "POST", "/api/v1/records", payload, headers
+ )
+ return Record(self.client, response.get("data"))
+
+ def create_many(
+ self,
+ label: str,
+ data: Union[Dict[str, Any], List[Dict[str, Any]]],
+ options: Optional[Dict[str, bool]] = None,
+ transaction: Optional[Transaction] = None,
+ ) -> List[Record]:
+ """Create multiple records.
+
+ Args:
+ label: Label for all records
+ data: List or Dict of record data
+ options: Optional parsing and response options (returnResult, suggestTypes)
+ transaction: Optional transaction object
+
+ Returns:
+ List of Record objects
+ """
+ headers = Transaction._build_transaction_header(transaction)
+
+ payload = {
+ "label": label,
+ "payload": data,
+ "options": options or {"returnResult": True, "suggestTypes": True},
+ }
+ response = self.client._make_request(
+ "POST", "/api/v1/records/import/json", payload, headers
+ )
+ return [Record(self.client, record) for record in response.get("data")]
+
+ def attach(
+ self,
+ source: Union[str, Dict[str, Any]],
+ target: Union[
+ str,
+ List[str],
+ Dict[str, Any],
+ List[Dict[str, Any]],
+ "Record",
+ List["Record"],
+ ],
+ options: Optional[RelationshipOptions] = None,
+ transaction: Optional[Transaction] = None,
+ ) -> Dict[str, str]:
+ """Attach records to a source record."""
+ headers = Transaction._build_transaction_header(transaction)
+
+ source_id = self._extract_target_ids(source)[0]
+ target_ids = self._extract_target_ids(target)
+ payload = {"targetIds": target_ids}
+ if options:
+ payload.update(typing.cast(typing.Dict[str, typing.Any], options))
+ return self.client._make_request(
+ "POST", f"/api/v1/records/{source_id}/relations", payload, headers
+ )
+
+ def detach(
+ self,
+ source: Union[str, Dict[str, Any]],
+ target: Union[
+ str,
+ List[str],
+ Dict[str, Any],
+ List[Dict[str, Any]],
+ "Record",
+ List["Record"],
+ ],
+ options: Optional[RelationshipDetachOptions] = None,
+ transaction: Optional[Transaction] = None,
+ ) -> Dict[str, str]:
+ """Detach records from a source record."""
+ headers = Transaction._build_transaction_header(transaction)
+
+ source_id = self._extract_target_ids(source)[0]
+ target_ids = self._extract_target_ids(target)
+ payload = {"targetIds": target_ids}
+ if options:
+ payload.update(typing.cast(typing.Dict[str, typing.Any], options))
+ return self.client._make_request(
+ "PUT", f"/api/v1/records/{source_id}/relations", payload, headers
+ )
+
+ def delete(
+ self, query: SearchQuery, transaction: Optional[Transaction] = None
+ ) -> Dict[str, str]:
+ """Delete records matching the query."""
+ headers = Transaction._build_transaction_header(transaction)
+
+ return self.client._make_request(
+ "PUT",
+ "/api/v1/records/delete",
+ typing.cast(typing.Dict[str, typing.Any], query or {}),
+ headers,
+ )
+
+ def delete_by_id(
+ self,
+ id_or_ids: Union[str, List[str]],
+ transaction: Optional[Transaction] = None,
+ ) -> Dict[str, str]:
+ """Delete records by ID(s)."""
+ headers = Transaction._build_transaction_header(transaction)
+
+ if isinstance(id_or_ids, list):
+ return self.client._make_request(
+ "PUT",
+ "/api/v1/records/delete",
+ {"limit": 1000, "where": {"$id": {"$in": id_or_ids}}},
+ headers,
+ )
+ return self.client._make_request(
+ "DELETE", f"/api/v1/records/{id_or_ids}", None, headers
+ )
+
+ def find(
+ self,
+ query: Optional[SearchQuery] = None,
+ record_id: Optional[str] = None,
+ transaction: Optional[Transaction] = None,
+ ) -> List[Record]:
+ """Find records matching the query."""
+
+ try:
+ headers = Transaction._build_transaction_header(transaction)
+
+ path = (
+ f"/api/v1/records/{record_id}/search"
+ if record_id
+ else "/api/v1/records/search"
+ )
+ response = self.client._make_request(
+ "POST",
+ path,
+ data=typing.cast(typing.Dict[str, typing.Any], query or {}),
+ headers=headers,
+ )
+ return [Record(self.client, record) for record in response.get("data")]
+ except Exception:
+ return []
+
+ def import_csv(
+ self,
+ label: str,
+ csv_data: Union[str, bytes],
+ options: Optional[Dict[str, bool]] = None,
+ transaction: Optional[Transaction] = None,
+ ) -> List[Dict[str, Any]]:
+ """Import data from CSV."""
+ headers = Transaction._build_transaction_header(transaction)
+
+ payload = {
+ "label": label,
+ "payload": csv_data,
+ "options": options or {"returnResult": True, "suggestTypes": True},
+ }
+
+ return self.client._make_request(
+ "POST", "/api/v1/records/import/csv", payload, headers
+ )
+
+ @staticmethod
+ def _extract_target_ids(
+ target: Union[
+ str,
+ List[str],
+ Dict[str, Any],
+ List[Dict[str, Any]],
+ "Record",
+ List["Record"],
+ ]
+ ) -> List[str]:
+ """Extract target IDs from various input types."""
+ if isinstance(target, str):
+ return [target]
+ elif isinstance(target, list):
+ return [t.get("__id", "") if isinstance(t, dict) else "" for t in target]
+ elif isinstance(target, Record) and "__id" in target.data:
+ return [target.data["__id"]]
+ elif isinstance(target, dict) and "__id" in target:
+ return [target["__id"]]
+ raise ValueError("Invalid target format")
diff --git a/src/rushdb/api/relationships.py b/src/rushdb/api/relationships.py
new file mode 100644
index 0000000..712a9cc
--- /dev/null
+++ b/src/rushdb/api/relationships.py
@@ -0,0 +1,60 @@
+import typing
+from typing import List, Optional, TypedDict, Union
+from urllib.parse import urlencode
+
+from ..models.relationship import Relationship
+from ..models.search_query import SearchQuery
+from ..models.transaction import Transaction
+from .base import BaseAPI
+
+
+class PaginationParams(TypedDict, total=False):
+ """TypedDict for pagination parameters."""
+
+ limit: int
+ skip: int
+
+
+class RelationsAPI(BaseAPI):
+ """API for managing relationships in RushDB."""
+
+ async def find(
+ self,
+ query: Optional[SearchQuery] = None,
+ pagination: Optional[PaginationParams] = None,
+ transaction: Optional[Union[Transaction, str]] = None,
+ ) -> List[Relationship]:
+ """Find relations matching the search parameters.
+
+ Args:
+ query: Search query parameters
+ pagination: Optional pagination parameters (limit and skip)
+ transaction: Optional transaction context or transaction ID
+
+ Returns:
+ List of matching relations
+ """
+ # Build query string for pagination
+ query_params = {}
+ if pagination:
+ if pagination.get("limit") is not None:
+ query_params["limit"] = str(pagination["limit"])
+ if pagination.get("skip") is not None:
+ query_params["skip"] = str(pagination["skip"])
+
+ # Construct path with query string
+ query_string = f"?{urlencode(query_params)}" if query_params else ""
+ path = f"/records/relations/search{query_string}"
+
+ # Build headers with transaction if present
+ headers = Transaction._build_transaction_header(transaction)
+
+ # Make request
+ response = self.client._make_request(
+ method="POST",
+ path=path,
+ data=typing.cast(typing.Dict[str, typing.Any], query or {}),
+ headers=headers,
+ )
+
+ return response.data
diff --git a/src/rushdb/api/transactions.py b/src/rushdb/api/transactions.py
new file mode 100644
index 0000000..8371045
--- /dev/null
+++ b/src/rushdb/api/transactions.py
@@ -0,0 +1,29 @@
+from typing import Optional
+
+from ..models.transaction import Transaction
+from .base import BaseAPI
+
+
+class TransactionsAPI(BaseAPI):
+ """API for managing transactions in RushDB."""
+
+ def begin(self, ttl: Optional[int] = None) -> Transaction:
+ """Begin a new transaction.
+
+ Returns:
+ Transaction object
+ """
+ response = self.client._make_request("POST", "/api/v1/tx", {"ttl": ttl or 5000})
+ return Transaction(self.client, response.get("data")["id"])
+
+ def _commit(self, transaction_id: str) -> None:
+ """Internal method to commit a transaction."""
+ return self.client._make_request(
+ "POST", f"/api/v1/tx/{transaction_id}/commit", {}
+ )
+
+ def _rollback(self, transaction_id: str) -> None:
+ """Internal method to rollback a transaction."""
+ return self.client._make_request(
+ "POST", f"/api/v1/tx/{transaction_id}/rollback", {}
+ )
diff --git a/src/rushdb/client.py b/src/rushdb/client.py
new file mode 100644
index 0000000..9325040
--- /dev/null
+++ b/src/rushdb/client.py
@@ -0,0 +1,106 @@
+"""RushDB Client"""
+
+import json
+import urllib.error
+import urllib.parse
+import urllib.request
+from typing import Any, Dict, Optional
+
+from .api.labels import LabelsAPI
+from .api.properties import PropertiesAPI
+from .api.records import RecordsAPI
+from .api.transactions import TransactionsAPI
+from .common import RushDBError
+
+
+class RushDBClient:
+ """Main client for interacting with RushDB."""
+
+ DEFAULT_BASE_URL = "https://api.rushdb.com"
+
+ def __init__(self, api_key: str, base_url: Optional[str] = None):
+ """Initialize the RushDB client.
+
+ Args:
+ api_key: The API key for authentication
+ base_url: Optional base URL for the RushDB server (default: https://api.rushdb.com)
+ """
+ self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
+ self.api_key = api_key
+ self.records = RecordsAPI(self)
+ self.properties = PropertiesAPI(self)
+ self.labels = LabelsAPI(self)
+ self.transactions = TransactionsAPI(self)
+
+ def _make_request(
+ self,
+ method: str,
+ path: str,
+ data: Optional[Dict] = None,
+ headers: Optional[Dict[str, str]] = None,
+ params: Optional[Dict[str, Any]] = None,
+ ) -> Any:
+ """Make an HTTP request to the RushDB server.
+
+ Args:
+ method: HTTP method (GET, POST, PUT, DELETE)
+ path: API endpoint path
+ data: Request body data
+ headers: Optional request headers
+ params: Optional URL query parameters
+
+ Returns:
+ The parsed JSON response
+ """
+ # Ensure path starts with /
+ if not path.startswith("/"):
+ path = "/" + path
+
+ # Clean and encode path components
+ path = path.strip()
+ path_parts = [
+ urllib.parse.quote(part, safe="") for part in path.split("/") if part
+ ]
+ clean_path = "/" + "/".join(path_parts)
+
+ # Build URL with query parameters
+ url = f"{self.base_url}{clean_path}"
+ if params:
+ query_string = urllib.parse.urlencode(params)
+ url = f"{url}?{query_string}"
+
+ # Prepare headers
+ request_headers = {
+ "token": self.api_key,
+ "Content-Type": "application/json",
+ **(headers or {}),
+ }
+
+ try:
+ # Prepare request body
+ body = None
+ if data is not None:
+ body = json.dumps(data).encode("utf-8")
+
+ # Create and send request
+ request = urllib.request.Request(
+ url, data=body, headers=request_headers, method=method
+ )
+
+ with urllib.request.urlopen(request) as response:
+ return json.loads(response.read().decode("utf-8"))
+ except urllib.error.HTTPError as e:
+ error_body = json.loads(e.read().decode("utf-8"))
+ raise RushDBError(error_body.get("message", str(e)), error_body)
+ except urllib.error.URLError as e:
+ raise RushDBError(f"Connection error: {str(e)}")
+ except json.JSONDecodeError as e:
+ raise RushDBError(f"Invalid JSON response: {str(e)}")
+
+ def ping(self) -> bool:
+ """Check if the server is reachable."""
+ try:
+ self._make_request("GET", "/")
+ return True
+ except RushDBError:
+ return False
diff --git a/src/rushdb/common.py b/src/rushdb/common.py
new file mode 100644
index 0000000..8191355
--- /dev/null
+++ b/src/rushdb/common.py
@@ -0,0 +1,9 @@
+from typing import Dict, Optional
+
+
+class RushDBError(Exception):
+ """Custom exception for RushDB client errors."""
+
+ def __init__(self, message: str, details: Optional[Dict] = None):
+ super().__init__(message)
+ self.details = details or {}
diff --git a/src/rushdb/models/__init__.py b/src/rushdb/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/rushdb/models/property.py b/src/rushdb/models/property.py
new file mode 100644
index 0000000..ea34b81
--- /dev/null
+++ b/src/rushdb/models/property.py
@@ -0,0 +1,55 @@
+from typing import Any, List, Literal, Optional, TypedDict, Union
+
+
+# Value types
+class DatetimeObject(TypedDict, total=False):
+ """Datetime object structure"""
+
+ year: int
+ month: Optional[int]
+ day: Optional[int]
+ hour: Optional[int]
+ minute: Optional[int]
+ second: Optional[int]
+ millisecond: Optional[int]
+ microsecond: Optional[int]
+ nanosecond: Optional[int]
+
+
+DatetimeValue = Union[DatetimeObject, str]
+BooleanValue = bool
+NumberValue = float
+StringValue = str
+
+# Property types
+PropertyType = Literal["boolean", "datetime", "null", "number", "string"]
+
+
+class Property(TypedDict):
+ """Base property structure"""
+
+ id: str
+ name: str
+ type: PropertyType
+ metadata: Optional[str]
+
+
+class PropertyWithValue(Property):
+ """Property with a value"""
+
+ value: Union[
+ DatetimeValue,
+ BooleanValue,
+ None,
+ NumberValue,
+ StringValue,
+ List[Union[DatetimeValue, BooleanValue, None, NumberValue, StringValue]],
+ ]
+
+
+class PropertyValuesData(TypedDict, total=False):
+ """Property values data structure"""
+
+ max: Optional[float]
+ min: Optional[float]
+ values: List[Any]
diff --git a/src/rushdb/models/record.py b/src/rushdb/models/record.py
new file mode 100644
index 0000000..9b11268
--- /dev/null
+++ b/src/rushdb/models/record.py
@@ -0,0 +1,112 @@
+from datetime import datetime
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
+
+from .relationship import RelationshipDetachOptions, RelationshipOptions
+from .transaction import Transaction
+
+if TYPE_CHECKING:
+ from ..client import RushDBClient
+
+
+class Record:
+ """Represents a record in RushDB with methods for manipulation."""
+
+ def __init__(
+ self, client: "RushDBClient", data: Union[Dict[str, Any], None] = None
+ ):
+ self._client = client
+ # Handle different data formats
+ if isinstance(data, dict):
+ self.data = data
+ elif isinstance(data, str):
+ # If just a string is passed, assume it's an ID
+ self.data = {}
+ else:
+ raise ValueError(f"Invalid data format for Record: {type(data)}")
+
+ @property
+ def id(self) -> str:
+ """Get record ID."""
+ record_id = self.data.get("__id")
+ if record_id is None:
+ raise ValueError("Record ID is missing or None")
+
+ return record_id
+
+ @property
+ def proptypes(self) -> str:
+ """Get record ID."""
+ return self.data["__proptypes"]
+
+ @property
+ def label(self) -> str:
+ """Get record ID."""
+ return self.data["__label"]
+
+ @property
+ def timestamp(self) -> int:
+ """Get record timestamp from ID."""
+ record_id = self.data.get("__id")
+ if record_id is None:
+ raise ValueError("Record ID is missing or None")
+
+ parts = record_id.split("-")
+ high_bits_hex = parts[0] + parts[1][:4]
+ return int(high_bits_hex, 16)
+
+ @property
+ def date(self) -> datetime:
+ """Get record creation date from ID."""
+ return datetime.fromtimestamp(self.timestamp / 1000)
+
+ def set(
+ self, data: Dict[str, Any], transaction: Optional[Transaction] = None
+ ) -> Dict[str, str]:
+ """Set record data through API request."""
+ return self._client.records.set(self.id, data, transaction)
+
+ def update(
+ self, data: Dict[str, Any], transaction: Optional[Transaction] = None
+ ) -> Dict[str, str]:
+ """Update record data through API request."""
+ return self._client.records.update(self.id, data, transaction)
+
+ def attach(
+ self,
+ target: Union[
+ str,
+ List[str],
+ Dict[str, Any],
+ List[Dict[str, Any]],
+ "Record",
+ List["Record"],
+ ],
+ options: Optional[RelationshipOptions] = None,
+ transaction: Optional[Transaction] = None,
+ ) -> Dict[str, str]:
+ """Attach other records to this record."""
+ return self._client.records.attach(self.id, target, options, transaction)
+
+ def detach(
+ self,
+ target: Union[
+ str,
+ List[str],
+ Dict[str, Any],
+ List[Dict[str, Any]],
+ "Record",
+ List["Record"],
+ ],
+ options: Optional[RelationshipDetachOptions] = None,
+ transaction: Optional[Transaction] = None,
+ ) -> Dict[str, str]:
+ """Detach records from this record."""
+ return self._client.records.detach(self.id, target, options, transaction)
+
+ def delete(self, transaction: Optional[Transaction] = None) -> Dict[str, str]:
+ """Delete this record."""
+ return self._client.records.delete_by_id(self.id, transaction)
+
+ def __repr__(self) -> str:
+ """String representation of record."""
+ return f"Record(id='{self.id}')"
diff --git a/src/rushdb/models/relationship.py b/src/rushdb/models/relationship.py
new file mode 100644
index 0000000..1ed1d61
--- /dev/null
+++ b/src/rushdb/models/relationship.py
@@ -0,0 +1,25 @@
+from typing import List, Literal, Optional, TypedDict, Union
+
+RelationshipDirection = Literal["in", "out"]
+
+
+class Relationship(TypedDict, total=False):
+ targetLabel: str
+ targetId: str
+ type: str
+ sourceId: str
+ sourceLabel: str
+
+
+class RelationshipOptions(TypedDict, total=False):
+ """Options for creating relations."""
+
+ direction: Optional[RelationshipDirection]
+ type: Optional[str]
+
+
+class RelationshipDetachOptions(TypedDict, total=False):
+ """Options for detaching relations."""
+
+ direction: Optional[RelationshipDirection]
+ typeOrTypes: Optional[Union[str, List[str]]]
diff --git a/src/rushdb/models/search_query.py b/src/rushdb/models/search_query.py
new file mode 100644
index 0000000..d2d0251
--- /dev/null
+++ b/src/rushdb/models/search_query.py
@@ -0,0 +1,18 @@
+from enum import Enum
+from typing import Any, Dict, List, Optional, TypedDict, Union
+
+
+class OrderDirection(str, Enum):
+ ASC = "asc"
+ DESC = "desc"
+
+
+class SearchQuery(TypedDict, total=False):
+ """TypedDict representing the query structure for finding records."""
+
+ where: Optional[Dict[str, Any]]
+ labels: Optional[List[str]]
+ skip: Optional[int]
+ limit: Optional[int]
+ orderBy: Optional[Union[Dict[str, OrderDirection], OrderDirection]]
+ aggregate: Optional[Dict[str, Any]]
diff --git a/src/rushdb/models/transaction.py b/src/rushdb/models/transaction.py
new file mode 100644
index 0000000..984990c
--- /dev/null
+++ b/src/rushdb/models/transaction.py
@@ -0,0 +1,54 @@
+from typing import TYPE_CHECKING, Dict, Optional, Union
+
+from ..common import RushDBError
+
+if TYPE_CHECKING:
+ from ..client import RushDBClient
+
+
+class Transaction:
+ """Represents a RushDB transaction."""
+
+ def __init__(self, client: "RushDBClient", transaction_id: str):
+ self.client = client
+ self.id = transaction_id
+ self._committed = False
+ self._rolled_back = False
+
+ def commit(self) -> None:
+ """Commit the transaction."""
+ if self._committed or self._rolled_back:
+ raise RushDBError("Transaction already completed")
+ self.client.transactions._commit(self.id)
+ self._committed = True
+
+ def rollback(self) -> None:
+ """Rollback the transaction."""
+ if self._committed or self._rolled_back:
+ raise RushDBError("Transaction already completed")
+ self.client.transactions._rollback(self.id)
+ self._rolled_back = True
+
+ @staticmethod
+ def _build_transaction_header(
+ transaction: Optional[Union[str, "Transaction"]] = None,
+ ) -> Optional[Dict[str, str]]:
+ """Build transaction header if transaction_id is provided."""
+ transaction_id = None
+
+ if isinstance(transaction, Transaction):
+ transaction_id = transaction.id
+ else:
+ transaction_id = transaction
+
+ return {"X-Transaction-Id": transaction_id} if transaction_id else None
+
+ def __enter__(self) -> "Transaction":
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if exc_type is not None:
+ if not self._rolled_back:
+ self.rollback()
+ elif not self._committed and not self._rolled_back:
+ self.commit()
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_base_setup.py b/tests/test_base_setup.py
new file mode 100644
index 0000000..d8af7d0
--- /dev/null
+++ b/tests/test_base_setup.py
@@ -0,0 +1,54 @@
+import os
+import unittest
+from pathlib import Path
+
+from dotenv import load_dotenv
+
+from src.rushdb import RushDBClient, RushDBError
+
+
+def load_env():
+ """Load environment variables from .env file."""
+ # Try to load from the root directory first
+ root_env = Path(__file__).parent.parent / ".env"
+ if root_env.exists():
+ load_dotenv(root_env)
+ else:
+ # Fallback to default .env.example if no .env exists
+ example_env = Path(__file__).parent.parent / ".env.example"
+ if example_env.exists():
+ load_dotenv(example_env)
+ print(
+ "Warning: Using .env.example for testing. Create a .env file with your credentials for proper testing."
+ )
+
+
+class TestBase(unittest.TestCase):
+ """Base test class with common setup."""
+
+ @classmethod
+ def setUpClass(cls):
+ """Set up test environment."""
+ load_env()
+
+ # Get configuration from environment variables
+ cls.token = os.getenv("RUSHDB_TOKEN")
+ cls.base_url = os.getenv("RUSHDB_URL", "http://localhost:8000")
+
+ if not cls.token:
+ raise ValueError(
+ "RUSHDB_TOKEN environment variable is not set. "
+ "Please create a .env file with your credentials. "
+ "You can use .env.example as a template."
+ )
+
+ def setUp(self):
+ """Set up test client."""
+ self.client = RushDBClient(self.token, base_url=self.base_url)
+
+ # Verify connection
+ try:
+ if not self.client.ping():
+ self.skipTest(f"Could not connect to RushDB at {self.base_url}")
+ except RushDBError as e:
+ self.skipTest(f"RushDB connection error: {str(e)}")
diff --git a/tests/test_create_import.py b/tests/test_create_import.py
new file mode 100644
index 0000000..6184c6b
--- /dev/null
+++ b/tests/test_create_import.py
@@ -0,0 +1,202 @@
+"""Test cases for RushDB create and import operations."""
+
+import json
+import unittest
+
+from src.rushdb import Record, RelationshipDetachOptions, RelationshipOptions
+
+from .test_base_setup import TestBase
+
+
+class TestCreateImport(TestBase):
+ """Test cases for record creation and import operations."""
+
+ def test_create_with_data(self):
+ """Test creating a record with data"""
+ data = {
+ "name": "Google LLC",
+ "address": "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA",
+ "foundedAt": "1998-09-04T00:00:00.000Z",
+ "rating": 4.9,
+ }
+ record = self.client.records.create("COMPANY", data)
+
+ print("\nDEBUG Record Data:")
+ print("Raw _data:", json.dumps(record.data, indent=2))
+ print("Available keys:", list(record.data.keys()))
+ print("Timestamp:", record.timestamp)
+ print("Date:", record.date)
+
+ self.assertIsInstance(record, Record)
+ self.assertEqual(record.data["__label"], "COMPANY")
+ self.assertEqual(record.data["name"], "Google LLC")
+ self.assertEqual(record.data["rating"], 4.9)
+
+ def test_record_methods(self):
+ """Test Record class methods"""
+ # Create a company record
+ company = self.client.records.create(
+ "COMPANY", {"name": "Apple Inc", "rating": 4.8}
+ )
+ self.assertIsInstance(company, Record)
+ self.assertEqual(company.data["name"], "Apple Inc")
+
+ # Create a department and attach it to the company
+ department = self.client.records.create(
+ "DEPARTMENT", {"name": "Engineering", "location": "Cupertino"}
+ )
+ self.assertIsInstance(department, Record)
+
+ # Test attach method
+ company.attach(
+ target=department.id,
+ options=RelationshipOptions(type="HAS_DEPARTMENT", direction="in"),
+ )
+
+ # Test detach method
+ company.detach(
+ target=department.id,
+ options=RelationshipDetachOptions(
+ typeOrTypes="HAS_DEPARTMENT", direction="in"
+ ),
+ )
+
+ # Test delete method
+ department.delete()
+
+ def test_create_with_transaction(self):
+ """Test creating records within a transaction"""
+ # Start a transaction
+ with self.client.transactions.begin() as transaction:
+ # Create company
+ company = self.client.records.create(
+ "COMPANY", {"name": "Apple Inc", "rating": 4.8}, transaction=transaction
+ )
+ self.assertIsInstance(company, Record)
+
+ # Create department
+ department = self.client.records.create(
+ "DEPARTMENT",
+ {"name": "Engineering", "location": "Cupertino"},
+ transaction=transaction,
+ )
+ self.assertIsInstance(department, Record)
+
+ # Create relation
+ company.attach(
+ target=department,
+ options=RelationshipOptions(type="HAS_DEPARTMENT", direction="out"),
+ transaction=transaction,
+ )
+
+ transaction.commit()
+
+ def test_create_many_records(self):
+ """Test creating multiple records"""
+ data = [
+ {
+ "name": "Apple Inc",
+ "address": "One Apple Park Way, Cupertino, CA 95014, USA",
+ "foundedAt": "1976-04-01T00:00:00.000Z",
+ "rating": 4.8,
+ },
+ {
+ "name": "Microsoft Corporation",
+ "address": "One Microsoft Way, Redmond, WA 98052, USA",
+ "foundedAt": "1975-04-04T00:00:00.000Z",
+ "rating": 4.7,
+ },
+ ]
+ records = self.client.records.create_many(
+ "COMPANY", data, {"returnResult": True, "suggestTypes": True}
+ )
+ self.assertTrue(all(isinstance(record, Record) for record in records))
+ self.assertEqual(len(records), 2)
+
+ print("\nDEBUG Record Data:")
+ print("Raw data:", json.dumps(records[1].data, indent=2))
+
+ self.assertEqual(records[0].label, "COMPANY")
+ self.assertEqual(records[1].label, "COMPANY")
+
+ def test_create_with_relations(self):
+ """Test creating records with relations"""
+ # Create employee
+ employee = self.client.records.create(
+ "EMPLOYEE", {"name": "John Doe", "position": "Senior Engineer"}
+ )
+
+ # Create project
+ project = self.client.records.create(
+ "PROJECT", {"name": "Secret Project", "budget": 1000000}
+ )
+
+ # Create relation with options
+ options = RelationshipOptions(type="HAS_EMPLOYEE", direction="out")
+ self.client.records.attach(source=project, target=employee, options=options)
+
+ # Test detaching with options
+ detach_options = RelationshipDetachOptions(
+ typeOrTypes="HAS_EMPLOYEE", direction="out"
+ )
+ self.client.records.detach(
+ source=project, target=employee, options=detach_options
+ )
+
+ def test_create_with_nested_data(self):
+ """Test creating records with nested data structure"""
+ data = {
+ "name": "Meta Platforms Inc",
+ "rating": 4.6,
+ "DEPARTMENT": [
+ {
+ "name": "Reality Labs",
+ "PROJECT": [
+ {
+ "name": "Quest 3",
+ "active": True,
+ "EMPLOYEE": [
+ {"name": "Mark Zuckerberg", "position": "CEO"}
+ ],
+ }
+ ],
+ }
+ ],
+ }
+ self.client.records.create_many("COMPANY", data)
+
+ def test_transaction_rollback(self):
+ """Test transaction rollback"""
+ transaction = self.client.transactions.begin()
+ try:
+ # Create some records
+ self.client.records.create(
+ "COMPANY",
+ {"name": "Failed Company", "rating": 1.0},
+ transaction=transaction,
+ )
+
+ # Simulate an error
+ raise ValueError("Simulated error")
+
+ # This won't be executed due to the error
+ self.client.records.create(
+ "DEPARTMENT", {"name": "Failed Department"}, transaction=transaction
+ )
+
+ except ValueError:
+ # Rollback the transaction
+ transaction.rollback()
+
+ def test_import_csv(self):
+ """Test importing data from CSV"""
+ csv_data = """name,age,department,role,salary
+John Doe,30,Engineering,Senior Engineer,120000
+Jane Smith,28,Product,Product Manager,110000
+Bob Wilson,35,Engineering,Tech Lead,140000"""
+
+ self.client.records.import_csv("EMPLOYEE", csv_data)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_search_query.py b/tests/test_search_query.py
new file mode 100644
index 0000000..dc859aa
--- /dev/null
+++ b/tests/test_search_query.py
@@ -0,0 +1,213 @@
+"""Test cases for RushDB search query functionality."""
+
+import unittest
+
+from .test_base_setup import TestBase
+
+
+class TestSearchQuery(TestBase):
+ def test_basic_equality_search(self):
+ """Test basic equality search"""
+ query = {"where": {"name": "John Doe"}} # Implicit equality
+ result = self.client.records.find(query)
+ print(result)
+
+ def test_basic_comparison_operators(self):
+ """Test basic comparison operators"""
+ query = {
+ "where": {
+ "age": {"$gt": 25},
+ "score": {"$lte": 100},
+ "status": {"$ne": "inactive"},
+ }
+ }
+ self.client.records.find(query)
+
+ def test_string_operations(self):
+ """Test string-specific operations"""
+ query = {
+ "where": {
+ "name": {"$startsWith": "J"},
+ "email": {"$contains": "@example.com"},
+ "code": {"$endsWith": "XYZ"},
+ }
+ }
+ self.client.records.find(query)
+
+ def test_array_operations(self):
+ """Test array operations (in/not in)"""
+ query = {
+ "where": {
+ "status": {"$in": ["active", "pending"]},
+ "category": {"$nin": ["archived", "deleted"]},
+ "tags": {"$contains": "important"},
+ }
+ }
+ self.client.records.find(query)
+
+ def test_logical_operators(self):
+ """Test logical operators (AND, OR, NOT)"""
+ query = {
+ "where": {
+ "$and": [{"age": {"$gte": 18}}, {"status": "active"}],
+ "$or": [{"role": "admin"}, {"permissions": {"$contains": "write"}}],
+ }
+ }
+ self.client.records.find(query)
+
+ def test_nested_logical_operators(self):
+ """Test nested logical operators"""
+ query = {
+ "where": {
+ "$or": [
+ {
+ "$and": [
+ {"age": {"$gte": 18}},
+ {"age": {"$lt": 65}},
+ {"status": "employed"},
+ ]
+ },
+ {"$and": [{"age": {"$gte": 65}}, {"status": "retired"}]},
+ ]
+ }
+ }
+ self.client.records.find(query)
+
+ def test_complex_nested_relations(self):
+ """Test complex nested relations"""
+ query = {
+ "where": {
+ "EMPLOYEE": {
+ "$and": [
+ {"position": {"$contains": "Manager"}},
+ {
+ "DEPARTMENT": {
+ "name": "Engineering",
+ "COMPANY": {
+ "industry": "Technology",
+ "revenue": {"$gt": 1000000},
+ },
+ }
+ },
+ ]
+ }
+ },
+ "orderBy": {"created_at": "desc"},
+ "limit": 10,
+ }
+ self.client.records.find(query)
+
+ def test_query_builder_simple(self):
+ """Test simple query conditions"""
+ query = {"where": {"$and": [{"age": {"$gt": 25}}, {"status": "active"}]}}
+ self.client.records.find(query)
+
+ def test_query_builder_complex(self):
+ """Test complex query conditions"""
+ query = {
+ "where": {
+ "$or": [
+ {
+ "$and": [
+ {"age": {"$gte": 18}},
+ {"age": {"$lt": 65}},
+ {"status": "employed"},
+ ]
+ },
+ {"$and": [{"age": {"$gte": 65}}, {"status": "retired"}]},
+ ]
+ },
+ "orderBy": {"age": "desc"},
+ "limit": 20,
+ }
+ self.client.records.find(query)
+
+ def test_advanced_graph_traversal(self):
+ """Test advanced graph traversal with multiple relations"""
+ query = {
+ "where": {
+ "USER": {
+ "$and": [
+ {"role": "customer"},
+ {
+ "PLACED_ORDER": {
+ "$and": [
+ {"status": "completed"},
+ {"total": {"$gt": 100}},
+ {
+ "CONTAINS_PRODUCT": {
+ "$and": [
+ {"category": "electronics"},
+ {"price": {"$gt": 50}},
+ {
+ "MANUFACTURED_BY": {
+ "country": "Japan",
+ "rating": {"$gte": 4},
+ }
+ },
+ ]
+ }
+ },
+ ]
+ }
+ },
+ ]
+ }
+ }
+ }
+ self.client.records.find(query)
+
+ def test_complex_query_with_all_features(self):
+ """Test combining all query features"""
+ query = {
+ "labels": ["User", "Customer"],
+ "where": {
+ "$and": [
+ {
+ "$or": [
+ {"age": {"$gte": 18}},
+ {
+ "$and": [
+ {"guardian": {"$exists": True}},
+ {"guardian_approved": True},
+ ]
+ },
+ ]
+ },
+ {"status": {"$in": ["active", "pending"]}},
+ {"email": {"$endsWith": "@company.com"}},
+ {
+ "BELONGS_TO_GROUP": {
+ "$and": [
+ {"name": {"$startsWith": "Premium"}},
+ {"status": "active"},
+ {
+ "HAS_SUBSCRIPTION": {
+ "$and": [
+ {"type": "premium"},
+ {"expires_at": {"$gt": "2024-01-01"}},
+ {
+ "INCLUDES_FEATURES": {
+ "name": {
+ "$in": ["feature1", "feature2"]
+ },
+ "enabled": True,
+ }
+ },
+ ]
+ }
+ },
+ ]
+ }
+ },
+ ]
+ },
+ "orderBy": {"created_at": "desc", "name": "asc"},
+ "skip": 0,
+ "limit": 50,
+ }
+ self.client.records.find(query)
+
+
+if __name__ == "__main__":
+ unittest.main()