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 Logo](https://raw.githubusercontent.com/rush-db/rushdb/main/rushdb-logo.svg) + +# 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()