diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..0d3d252
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @caffeine-addictt
diff --git a/.github/CODESTYLE.md b/.github/CODESTYLE.md
new file mode 100644
index 0000000..06e5ff8
--- /dev/null
+++ b/.github/CODESTYLE.md
@@ -0,0 +1,118 @@
+# Code Style
+The following is a general guide on how to style your work so that the project
+remains consistent throughout all files. Please read this document in it's entirety
+and refer to it throughout the development of your contribution.
+
+1. [General Guidelines](#general-guidelines)
+2. [Commit Message Guidelines](#commit-message-guidelines)
+3. [Markdown Guidelines](#markdown-guidelines)
+
+
+
+## General Guidelines
+Listed is a example class used demonstrate general rules you should follow throughout the development of your contribution.
+
+- Docstrings are to follow reST (reStructuredText Docstring Format) as specified in [PEP 287](https://peps.python.org/pep-0287/)
+- Private attributes are to be prefixed with an underscore
+- Use of [typing](https://docs.python.org/3/library/typing.html) type hints
+- All files are to use 2 space indenting
+
+```python
+class ExampleClass:
+ """
+ ExampleClass
+ ------------
+ Example class for CODESTYLE.md
+ """
+ # ^^^ reST Docstring Format
+
+ _private_attribute : int # private attributes begin with a lowercase
+ public_attribute : int # type hint for integer is defined here
+
+ def __init__(
+ self,
+ public_attribute: int # type hint for parameters
+ ) -> None: # the expected return value of method
+ """
+ Initializes a ExampleClass
+
+ Parameters
+ ----------
+ :param public_attribute: example attribute
+ """
+ self.public_attribute = public_attribute
+ self.private_attribute = square(public_attribute)
+
+ def square(self, value: int) -> int:
+ """
+ Example method that square roots a value
+
+ Parameters
+ ----------
+ :param value: value that you want squared
+ """
+ return value**2
+```
+
+
+
+## Commit Message Guidelines
+When committing, commit messages are prefixed with one of the following depending on the type of change made.
+
+ - `feat:` when a new feature is introduced with the changes.
+ - `fix:` when a bug fix has occurred.
+ - `chore:` for changes that do not relate to a fix or feature and do not modify *source* or *tests*. (like updating dependencies)
+ - `refactor:` for refactoring code that neither fixes a bug nor adds a feature.
+ - `docs:` when changes are made to documentation.
+ - `style:` when changes that do not affect the code, but modify formatting.
+ - `test:` when changes to tests are made.
+ - `perf:` for changes that improve performance.
+ - `ci:` for changes that affect CI.
+ - `build:` for changes that affect the build system or external dependencies.
+ - `revert:` when reverting changes.
+
+Commit messages are also to begin with an uppercase character. Below list some example commit messages.
+
+```sh
+git commit -m "docs: Added README.md"
+git commit -m "revert: Removed README.md"
+git commit -m "docs: Moved README.md"
+```
+
+
+
+## Markdown Guidelines
+Currently, documentation for this project resides in markdown files.
+ - Headings are to be separated with 3 lines
+ - Use of HTML comments is appreciated
+ - Use of HTML is permitted
+ - [reference style links](https://www.markdownguide.org/basic-syntax/#reference-style-links) are not required by are appreciated
+ - Exceedingly long lines are to be broken
+ - The indents are to be two spaces
+
+```markdown
+
+# Section
+Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+sed do eiusmod tempor incididunt ut labore et dolore
+magna aliqua. Ut enim ad minim veniam, quis nostrud
+exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in
+reprehenderit in voluptate velit esse cillum dolore eu
+fugiat nulla pariatur. Excepteur sint occaecat cupidatat
+non proident, sunt in culpa qui officia deserunt mollit
+anim id est laborum. found [Lorem Ipsum Generator]
+
+
+
+# Section 2
+
+ - Apple
+
- Orange
+
- Pineapple
+
+
+
+
+[Lorem Ipsum Generator]: https://loremipsum.io/generator/
+```
diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..c2af0a1
--- /dev/null
+++ b/.github/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+thread@ngjx.org.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..0e2b1fc
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,27 @@
+# **Contributing**
+
+When contributing to this repository, please first discuss the change you wish to make via issue,
+email, or any other method with the owners of this repository before making a change.
+
+Please note we have a [code of conduct](CODE_OF_CONDUCT.md); please follow it in all your interactions with the project.
+
+
+
+## Pull Request Process
+
+1. Ensure any install or build dependencies are removed before the end of the layer when doing a
+ build.
+2. Update the README.md with details of changes to the interface; this includes new environment variables, exposed ports, valid file locations and container parameters.
+3. Increase the version numbers in any examples files and the README.md to the new version that this
+ Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
+4. You may merge the Pull Request once you have the sign-off of two other developers, or if you
+ do not have permission to do that, you may request the second reviewer to merge it for you.
+
+
+
+## Issue Report Process
+
+1. Go to the project's issues.
+2. Select the template that better fits your issue.
+3. Read the instructions carefully and write within the template guidelines.
+4. Submit it and wait for support.
diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.md b/.github/ISSUE_TEMPLATE/1-bug-report.md
new file mode 100644
index 0000000..5dc6668
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/1-bug-report.md
@@ -0,0 +1,82 @@
+---
+name: "Bug Report"
+about: "Report an issue to help the project improve."
+title: "[Bug] "
+labels: "Type: Bug"
+assignees: caffeine-addictt
+
+---
+
+# Bug report
+Your issue may already be reported!
+Please check out our [active issues](https://github.com/python-thread/thread-cli/issues) before creating one.
+
+
+
+## Expected Behavior
+
+
+
+
+## Current Behavior
+
+
+
+
+## Is this a regression?
+
+
+
+
+## Possible Solution
+
+
+
+
+## Steps to Reproduce (for bugs)
+
+1.
+2.
+3.
+4.
+
+
+
+## Context
+
+
+
+
+## Your Environment
+
+* Version used:
+* Python version:
+* Link to your project:
+* Operating System and version (desktop or mobile):
diff --git a/.github/ISSUE_TEMPLATE/2-failing-test.md b/.github/ISSUE_TEMPLATE/2-failing-test.md
new file mode 100644
index 0000000..f840ca0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/2-failing-test.md
@@ -0,0 +1,40 @@
+---
+name: "Failing Test"
+about: "Report failing tests or CI jobs."
+title: "[Test] "
+labels: "Type: Test"
+assignees: caffeine-addictt
+
+---
+
+# Failing Test
+Your issue may already be reported!
+Please check out our [active issues](https://github.com/python-thread/thread-cli/issues) before creating one.
+
+
+
+## Which Jobs/Tests are Failing?
+*
+
+
+
+## Reason for Failure
+
+
+
+
+## Media Prove
+
+
+
+
+## Additional Context
+
diff --git a/.github/ISSUE_TEMPLATE/3-docs-bug.md b/.github/ISSUE_TEMPLATE/3-docs-bug.md
new file mode 100644
index 0000000..18c0d96
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/3-docs-bug.md
@@ -0,0 +1,41 @@
+---
+name: "Documentation or README.md issue report"
+about: "Report an issue in the project's documentation or README.md file."
+title: ""
+labels: "Documentation"
+assignees: caffeine-addictt
+
+---
+
+# Documentation Issue Report
+Your issue may already be reported!
+Please check out our [active issues](https://github.com/python-thread/thread-cli/issues) before creating one.
+
+
+
+## Describe the Bug
+
+
+
+
+## Steps to Reproduce
+
+
+1.
+2.
+3.
+4.
+
+
+
+## Additional Context
+
diff --git a/.github/ISSUE_TEMPLATE/4-feature-request.md b/.github/ISSUE_TEMPLATE/4-feature-request.md
new file mode 100644
index 0000000..75b1d92
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/4-feature-request.md
@@ -0,0 +1,43 @@
+---
+name: "Feature Request"
+about: "Suggest an idea or possible new feature for this project."
+title: ""
+labels: "Type: Feature"
+assignees: caffeine-addictt
+
+---
+
+# Feature Request
+Your issue may already be reported!
+Please check out our [active issues](https://github.com/python-thread/thread-cli/issues) before creating one.
+
+
+
+## Is Your Feature Request Related to an Issue?
+
+
+
+
+## Describe the Solution You'd Like
+
+
+
+
+## Describe Alternatives You've Considered
+
+
+
+
+## Additional Context
+
diff --git a/.github/ISSUE_TEMPLATE/5-enhancement-request.md b/.github/ISSUE_TEMPLATE/5-enhancement-request.md
new file mode 100644
index 0000000..a0cc576
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/5-enhancement-request.md
@@ -0,0 +1,43 @@
+---
+name: "Enhancement Request"
+about: "Suggest an enhancement for this project. Improve an existing feature"
+title: ""
+labels: "Type: Enhancement"
+assignees: caffeine-addictt
+
+---
+
+# Enhancement Request
+Your issue may already be reported!
+Please check out our [active issues](https://github.com/python-thread/thread-cli/issues) before creating one.
+
+
+
+## Is Your Enhancement Request Related to an Issue?
+
+
+
+
+## Describe the Solution You'd Like
+
+
+
+
+## Describe Alternatives You've Considered
+
+
+
+
+## Additional Context
+
diff --git a/.github/ISSUE_TEMPLATE/6-security-report.md b/.github/ISSUE_TEMPLATE/6-security-report.md
new file mode 100644
index 0000000..e91fb6a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/6-security-report.md
@@ -0,0 +1,97 @@
+---
+name: "Security Report"
+about: "Report an issue to help the project improve."
+title: ""
+labels: "Type: Security"
+assignees: caffeine-addictt
+
+---
+
+
+
+# Security Report
+Your issue may already be reported!
+Please check out our [active issues](https://github.com/python-thread/thread-cli/issues) before creating one.
+
+
+
+## Describe the Security Issue
+
+
+
+
+## Steps to Reproduce
+
+
+
+
+## Expected Behavior
+
+
+
+
+## Media Prove
+
+
+
+
+## Additional Context
+
+
+
+
+### Your Environment
+
+* Version used:
+* Python version:
+* Link to your project:
+* Operating System and version (desktop or mobile):
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/7-question-support.md b/.github/ISSUE_TEMPLATE/7-question-support.md
new file mode 100644
index 0000000..cc44220
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/7-question-support.md
@@ -0,0 +1,20 @@
+---
+name: "Question or Support Request"
+about: "Questions and requests for support."
+title: ""
+labels: "Type: Question"
+assignees: caffeine-addictt
+
+---
+
+# Question or Support Request
+
+
+
+
+## Describe your question or ask for support
+
+
+*
\ No newline at end of file
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..aff3721
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,79 @@
+
+
+# What type of PR is this? (check all applicable)
+
+- [ ] Refactor
+- [ ] Feature
+- [ ] Bug Fix
+- [ ] Optimization
+- [ ] Breaking Change
+- [ ] Documentation Update
+
+
+
+## Description
+
+
+
+
+## Related Tickets & Documents
+
+
+
+- Related Issue #
+- Closes #
+
+
+
+## QA Instructions, Screenshots, Recordings
+
+_Please replace this line with instructions on how to test your changes, a note
+on the devices and browsers this has been tested on, as well as any relevant
+images for UI changes._
+
+
+
+## Added/updated tests?
+
+_We encourage you to keep the code coverage percentage at 80% and above._
+
+- [ ] Yes
+- [ ] No, and this is why: _please replace this line with details on why tests
+ have not been included_
+- [ ] I need help with writing tests
+
+
+
+## [optional] Are there any post deployment tasks we need to perform?
+
+
+
+
+## Checklist
+
+- [ ] My code follows the code style of this project.
+- [ ] My change requires a change to the documentation.
+- [ ] I have updated the documentation accordingly.
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
new file mode 100644
index 0000000..89ba50c
--- /dev/null
+++ b/.github/SECURITY.md
@@ -0,0 +1,11 @@
+# **Reporting Security Issues**
+
+The project's team and community take security issues.
+
+We appreciate your efforts to disclose your findings responsibly and will make every effort to acknowledge your contributions.
+
+To report a security issue, go to the project's issues and create a new issue using the ⚠️ Security Report 'issue template'.
+
+Read the instructions of this issue template carefully, and if your report could leak data or might expose how to gain access to a restricted area or break the system, please email [thread@ngjx.org](mailto:thread@ngjx.org) and include the word "SECURITY" in the subject line.
+
+We'll endeavour to respond quickly and keep you updated throughout the process.
diff --git a/.github/settings.yml b/.github/settings.yml
new file mode 100644
index 0000000..a9fff4b
--- /dev/null
+++ b/.github/settings.yml
@@ -0,0 +1,80 @@
+labels:
+ - name: "Type: Bug"
+ color: e80c0c
+ description: Something isn't working as expected.
+
+ - name: "Type: Enhancement"
+ color: 54b2ff
+ description: Suggest an improvement for an existing feature.
+
+ - name: "Type: Feature"
+ color: 54b2ff
+ description: Suggest a new feature.
+
+ - name: "Type: Security"
+ color: fbff00
+ description: A problem or enhancement related to a security issue.
+
+ - name: "Type: Question"
+ color: 9309ab
+ description: Request for information.
+
+ - name: "Type: Test"
+ color: ce54e3
+ description: A problem or enhancement related to a test.
+
+ - name: "Status: Awaiting Review"
+ color: 24d15d
+ description: Ready for review.
+
+ - name: "Status: WIP"
+ color: 07b340
+ description: Currently being worked on.
+
+ - name: "Status: Waiting"
+ color: 38C968
+ description: Waiting on something else to be ready.
+
+ - name: "Status: Stale"
+ color: 66b38a
+ description: Has had no activity for some time.
+
+ - name: "Duplicate"
+ color: EB862D
+ description: Duplicate of another issue.
+
+ - name: "Invalid"
+ color: faef50
+ description: This issue doesn't seem right.
+
+ - name: "Priority: High +"
+ color: ff008c
+ description: Task is considered higher-priority.
+
+ - name: "Priority: Low -"
+ color: 690a34
+ description: Task is considered lower-priority.
+
+ - name: "Documentation"
+ color: 2fbceb
+ description: An issue/change with the documentation.
+
+ - name: "Won't fix"
+ color: C8D9E6
+ description: Reported issue is working as intended.
+
+ - name: "3rd party issue"
+ color: e88707
+ description: This issue might be caused by a 3rd party script/package/other reasons
+
+ - name: "Os: Windows"
+ color: AEB1C2
+ description: Is Windows-specific
+
+ - name: "Os: Mac"
+ color: AEB1C2
+ description: Is Mac-specific
+
+ - name: "Os: Linux"
+ color: AEB1C2
+ description: Is Linux-specific
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..6277b81
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,22 @@
+name: Upload Python Package
+
+on:
+ workflow_dispatch:
+ push:
+ tags:
+ - v*.*.*
+
+permissions:
+ contents: read
+
+jobs:
+ pypi-publish:
+ name: Upload release to PyPI
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Build and publish
+ uses: JRubics/poetry-publish@v1.17
+ with:
+ pypi_token: ${{ secrets.PYPI_TOKEN }}
diff --git a/.github/workflows/test-worker.yml b/.github/workflows/test-worker.yml
new file mode 100644
index 0000000..c5b8911
--- /dev/null
+++ b/.github/workflows/test-worker.yml
@@ -0,0 +1,42 @@
+name: Run Python tests
+
+on: [push]
+
+jobs:
+ build:
+ name: Run tests
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.9", "3.10", "3.11", "3.x"]
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install dependencies
+ run: |
+ python -m pip install -U pip
+ python -m pip install -U coverage pytest pytest-cov poetry
+ python -m poetry install
+ python -m poetry self add poetry-plugin-export
+ python -m poetry export -f requirements.txt --output requirements.txt
+ python -m pip install -r requirements.txt
+
+ - name: Lint with Ruff
+ run: |
+ python -m pip install -U ruff
+ ruff --per-file-ignores="__init__.py:F401" --per-file-ignores="__init__.py:E402" .
+ continue-on-error: true
+
+ - name: Test with pytest
+ run: |
+ coverage run -m pytest -v -s
+
+ - name: Generate Coverage Report
+ run: |
+ coverage report -m
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4fbc277
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+# Python stuff
+venv*/
+*.pytest_cache
+__pycache__/
+
+*.py[cod]
+*$py.class
+
+*.ruff_cache
+
+# Build
+dist/
+*.egg-info/
+
+.vscode/
diff --git a/LICENSE b/LICENSE
index 1173abf..578a941 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
BSD 3-Clause License
-Copyright (c) 2024, Thread
+Copyright (c) 2024, Jun Xiang
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..acd7691
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,380 @@
+# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+
+[[package]]
+name = "click"
+version = "8.1.7"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+ {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "coverage"
+version = "7.4.1"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"},
+ {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"},
+ {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"},
+ {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"},
+ {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"},
+ {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"},
+ {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"},
+ {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"},
+ {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"},
+ {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"},
+ {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"},
+ {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"},
+ {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"},
+ {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"},
+ {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"},
+ {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"},
+ {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"},
+ {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"},
+ {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"},
+ {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"},
+ {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"},
+ {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"},
+ {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"},
+ {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"},
+ {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"},
+ {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"},
+ {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"},
+ {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"},
+ {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"},
+ {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"},
+ {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"},
+ {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"},
+ {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"},
+ {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"},
+ {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"},
+ {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"},
+ {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"},
+ {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"},
+ {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"},
+ {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"},
+ {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"},
+ {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"},
+ {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"},
+ {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"},
+ {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"},
+ {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"},
+ {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"},
+ {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"},
+ {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"},
+ {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"},
+ {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"},
+ {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"},
+]
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.0"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
+ {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+description = "Python port of markdown-it. Markdown parsing, done right!"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
+ {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
+]
+
+[package.dependencies]
+mdurl = ">=0.1,<1.0"
+
+[package.extras]
+benchmarking = ["psutil", "pytest", "pytest-benchmark"]
+code-style = ["pre-commit (>=3.0,<4.0)"]
+compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
+linkify = ["linkify-it-py (>=1,<3)"]
+plugins = ["mdit-py-plugins"]
+profiling = ["gprof2dot"]
+rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+description = "Markdown URL utilities"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
+ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
+]
+
+[[package]]
+name = "numpy"
+version = "1.26.3"
+description = "Fundamental package for array computing in Python"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "numpy-1.26.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:806dd64230dbbfaca8a27faa64e2f414bf1c6622ab78cc4264f7f5f028fee3bf"},
+ {file = "numpy-1.26.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f98011ba4ab17f46f80f7f8f1c291ee7d855fcef0a5a98db80767a468c85cd"},
+ {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d45b3ec2faed4baca41c76617fcdcfa4f684ff7a151ce6fc78ad3b6e85af0a6"},
+ {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdd2b45bf079d9ad90377048e2747a0c82351989a2165821f0c96831b4a2a54b"},
+ {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:211ddd1e94817ed2d175b60b6374120244a4dd2287f4ece45d49228b4d529178"},
+ {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1240f767f69d7c4c8a29adde2310b871153df9b26b5cb2b54a561ac85146485"},
+ {file = "numpy-1.26.3-cp310-cp310-win32.whl", hash = "sha256:21a9484e75ad018974a2fdaa216524d64ed4212e418e0a551a2d83403b0531d3"},
+ {file = "numpy-1.26.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e1591f6ae98bcfac2a4bbf9221c0b92ab49762228f38287f6eeb5f3f55905ce"},
+ {file = "numpy-1.26.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b831295e5472954104ecb46cd98c08b98b49c69fdb7040483aff799a755a7374"},
+ {file = "numpy-1.26.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e87562b91f68dd8b1c39149d0323b42e0082db7ddb8e934ab4c292094d575d6"},
+ {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c66d6fec467e8c0f975818c1796d25c53521124b7cfb760114be0abad53a0a2"},
+ {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f25e2811a9c932e43943a2615e65fc487a0b6b49218899e62e426e7f0a57eeda"},
+ {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af36e0aa45e25c9f57bf684b1175e59ea05d9a7d3e8e87b7ae1a1da246f2767e"},
+ {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:51c7f1b344f302067b02e0f5b5d2daa9ed4a721cf49f070280ac202738ea7f00"},
+ {file = "numpy-1.26.3-cp311-cp311-win32.whl", hash = "sha256:7ca4f24341df071877849eb2034948459ce3a07915c2734f1abb4018d9c49d7b"},
+ {file = "numpy-1.26.3-cp311-cp311-win_amd64.whl", hash = "sha256:39763aee6dfdd4878032361b30b2b12593fb445ddb66bbac802e2113eb8a6ac4"},
+ {file = "numpy-1.26.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a7081fd19a6d573e1a05e600c82a1c421011db7935ed0d5c483e9dd96b99cf13"},
+ {file = "numpy-1.26.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12c70ac274b32bc00c7f61b515126c9205323703abb99cd41836e8125ea0043e"},
+ {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f784e13e598e9594750b2ef6729bcd5a47f6cfe4a12cca13def35e06d8163e3"},
+ {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f24750ef94d56ce6e33e4019a8a4d68cfdb1ef661a52cdaee628a56d2437419"},
+ {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:77810ef29e0fb1d289d225cabb9ee6cf4d11978a00bb99f7f8ec2132a84e0166"},
+ {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8ed07a90f5450d99dad60d3799f9c03c6566709bd53b497eb9ccad9a55867f36"},
+ {file = "numpy-1.26.3-cp312-cp312-win32.whl", hash = "sha256:f73497e8c38295aaa4741bdfa4fda1a5aedda5473074369eca10626835445511"},
+ {file = "numpy-1.26.3-cp312-cp312-win_amd64.whl", hash = "sha256:da4b0c6c699a0ad73c810736303f7fbae483bcb012e38d7eb06a5e3b432c981b"},
+ {file = "numpy-1.26.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1666f634cb3c80ccbd77ec97bc17337718f56d6658acf5d3b906ca03e90ce87f"},
+ {file = "numpy-1.26.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18c3319a7d39b2c6a9e3bb75aab2304ab79a811ac0168a671a62e6346c29b03f"},
+ {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b7e807d6888da0db6e7e75838444d62495e2b588b99e90dd80c3459594e857b"},
+ {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4d362e17bcb0011738c2d83e0a65ea8ce627057b2fdda37678f4374a382a137"},
+ {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b8c275f0ae90069496068c714387b4a0eba5d531aace269559ff2b43655edd58"},
+ {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc0743f0302b94f397a4a65a660d4cd24267439eb16493fb3caad2e4389bccbb"},
+ {file = "numpy-1.26.3-cp39-cp39-win32.whl", hash = "sha256:9bc6d1a7f8cedd519c4b7b1156d98e051b726bf160715b769106661d567b3f03"},
+ {file = "numpy-1.26.3-cp39-cp39-win_amd64.whl", hash = "sha256:867e3644e208c8922a3be26fc6bbf112a035f50f0a86497f98f228c50c607bb2"},
+ {file = "numpy-1.26.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3c67423b3703f8fbd90f5adaa37f85b5794d3366948efe9a5190a5f3a83fc34e"},
+ {file = "numpy-1.26.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46f47ee566d98849323f01b349d58f2557f02167ee301e5e28809a8c0e27a2d0"},
+ {file = "numpy-1.26.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a8474703bffc65ca15853d5fd4d06b18138ae90c17c8d12169968e998e448bb5"},
+ {file = "numpy-1.26.3.tar.gz", hash = "sha256:697df43e2b6310ecc9d95f05d5ef20eacc09c7c4ecc9da3f235d39e71b7da1e4"},
+]
+
+[[package]]
+name = "packaging"
+version = "23.2"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
+ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
+]
+
+[[package]]
+name = "pluggy"
+version = "1.4.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
+ {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pygments"
+version = "2.17.2"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
+ {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
+]
+
+[package.extras]
+plugins = ["importlib-metadata"]
+windows-terminal = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "pytest"
+version = "8.0.0"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"},
+ {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=1.3.0,<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 = "rich"
+version = "13.7.0"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"},
+ {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"},
+]
+
+[package.dependencies]
+markdown-it-py = ">=2.2.0"
+pygments = ">=2.13.0,<3.0.0"
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<9)"]
+
+[[package]]
+name = "ruff"
+version = "0.2.0"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ruff-0.2.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:638ea3294f800d18bae84a492cb5a245c8d29c90d19a91d8e338937a4c27fca0"},
+ {file = "ruff-0.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff35433fcf4dff6d610738712152df6b7d92351a1bde8e00bd405b08b3d5759"},
+ {file = "ruff-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf9faafbdcf4f53917019f2c230766da437d4fd5caecd12ddb68bb6a17d74399"},
+ {file = "ruff-0.2.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8153a3e4128ed770871c47545f1ae7b055023e0c222ff72a759f5a341ee06483"},
+ {file = "ruff-0.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a75a98ae989a27090e9c51f763990ad5bbc92d20626d54e9701c7fe597f399"},
+ {file = "ruff-0.2.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:87057dd2fdde297130ff99553be8549ca38a2965871462a97394c22ed2dfc19d"},
+ {file = "ruff-0.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d232f99d3ab00094ebaf88e0fb7a8ccacaa54cc7fa3b8993d9627a11e6aed7a"},
+ {file = "ruff-0.2.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d3c641f95f435fc6754b05591774a17df41648f0daf3de0d75ad3d9f099ab92"},
+ {file = "ruff-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3826fb34c144ef1e171b323ed6ae9146ab76d109960addca730756dc19dc7b22"},
+ {file = "ruff-0.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eceab7d85d09321b4de18b62d38710cf296cb49e98979960a59c6b9307c18cfe"},
+ {file = "ruff-0.2.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:30ad74687e1f4a9ff8e513b20b82ccadb6bd796fe5697f1e417189c5cde6be3e"},
+ {file = "ruff-0.2.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a7e3818698f8460bd0f8d4322bbe99db8327e9bc2c93c789d3159f5b335f47da"},
+ {file = "ruff-0.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:edf23041242c48b0d8295214783ef543847ef29e8226d9f69bf96592dba82a83"},
+ {file = "ruff-0.2.0-py3-none-win32.whl", hash = "sha256:e155147199c2714ff52385b760fe242bb99ea64b240a9ffbd6a5918eb1268843"},
+ {file = "ruff-0.2.0-py3-none-win_amd64.whl", hash = "sha256:ba918e01cdd21e81b07555564f40d307b0caafa9a7a65742e98ff244f5035c59"},
+ {file = "ruff-0.2.0-py3-none-win_arm64.whl", hash = "sha256:3fbaff1ba9564a2c5943f8f38bc221f04bac687cc7485e45237579fee7ccda79"},
+ {file = "ruff-0.2.0.tar.gz", hash = "sha256:63856b91837606c673537d2889989733d7dffde553828d3b0f0bacfa6def54be"},
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+description = "Tool to Detect Surrounding Shell"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
+ {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
+]
+
+[[package]]
+name = "thread"
+version = "0.1.3"
+description = "Threading module extension"
+optional = false
+python-versions = ">=3.9,<4.0"
+files = [
+ {file = "thread-0.1.3-py3-none-any.whl", hash = "sha256:d7dbef51cdfb239b74df3253cdb60e004a13f1cb1dd51595e4ac41eec057ede9"},
+ {file = "thread-0.1.3.tar.gz", hash = "sha256:6dcbaaa77c2c9a1f7803c6d65b16ffa93b3de0a9e7abc375b2b2f260dc8aab30"},
+]
+
+[package.dependencies]
+numpy = ">=1.26.2,<2.0.0"
+typer = {version = ">=0.9.0,<0.10.0", extras = ["all"]}
+typing-extensions = ">=4.8.0,<5.0.0"
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
+[[package]]
+name = "typer"
+version = "0.9.0"
+description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"},
+ {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"},
+]
+
+[package.dependencies]
+click = ">=7.1.1,<9.0.0"
+colorama = {version = ">=0.4.3,<0.5.0", optional = true, markers = "extra == \"all\""}
+rich = {version = ">=10.11.0,<14.0.0", optional = true, markers = "extra == \"all\""}
+shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""}
+typing-extensions = ">=3.7.4.3"
+
+[package.extras]
+all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
+dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"]
+doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"]
+test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.9.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
+ {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
+]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.9"
+content-hash = "53c2df993a65a583d8c040432670a602eb9c12a4e6577ae6eef265db68c72f54"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..92a45cb
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,41 @@
+[tool.poetry]
+name = "thread-cli"
+version = "0.1.0"
+description = "Threading module extension's CLI"
+authors = ["Alex "]
+license = "BSD-3-Clause"
+readme = "README.md"
+packages = [
+ { include = "thread-cli", from = "src" },
+ { include = "thread-cli/py.typed", from = "src" },
+]
+include = [{ path = "tests", format = "sdist" }]
+homepage = "https://github.com/python-thread/thread-cli"
+repository = "https://github.com/python-thread/thread-cli"
+documentation = "https://thread.ngjx.org"
+keywords = ["thread", "threading", "cli", "cli-extension"]
+classifiers = [
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: BSD License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+]
+
+[tool.poetry.urls]
+Changelog = "https://github.com/python-thread/thread-cli/releases"
+"Bug Tracker" = "https://github.com/python-thread/thread-cli/issues"
+
+[tool.poetry.dependencies]
+python = "^3.9"
+thread = "^0.1.3"
+
+[tool.poetry.group.dev.dependencies]
+pytest = "^8.0.0"
+coverage = "^7.4.1"
+ruff = "^0.2.0"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/src/thread-cli/__init__.py b/src/thread-cli/__init__.py
new file mode 100644
index 0000000..32a3091
--- /dev/null
+++ b/src/thread-cli/__init__.py
@@ -0,0 +1,32 @@
+"""
+## ThreadCLI Library
+Documentation: https://thread.ngjx.org
+
+
+---
+
+Released under the GPG-3 License
+
+Copyright (c) 2020, thread.ngjx.org. All rights reserved.
+"""
+
+__version__ = "0.1.0"
+from .utils.logging import ColorLogger, logging
+
+# Export Core
+from .base import cli_base as app
+from .process import process as process_cli
+
+app.command(
+ name="process",
+ no_args_is_help=True,
+ context_settings={"allow_extra_args": True},
+)(process_cli)
+
+
+# Setup Logging
+logging.setLoggerClass(ColorLogger)
+
+
+# Wildcard export
+__all__ = ["app"]
diff --git a/src/thread-cli/base.py b/src/thread-cli/base.py
new file mode 100644
index 0000000..c2e6eb6
--- /dev/null
+++ b/src/thread-cli/base.py
@@ -0,0 +1,117 @@
+import typer
+import logging
+
+from . import __version__
+from .utils import DebugOption, VerboseOption, QuietOption, verbose_args_processor
+logger = logging.getLogger('base')
+
+
+cli_base = typer.Typer(
+ no_args_is_help = True,
+ rich_markup_mode = 'rich',
+ context_settings = {
+ 'help_option_names': ['-h', '--help', 'help']
+ }
+)
+
+
+def version_callback(value: bool):
+ if value:
+ typer.echo(f'v{__version__}')
+ raise typer.Exit()
+
+
+@cli_base.callback(invoke_without_command = True)
+def callback(
+ version: bool = typer.Option(
+ None, '--version',
+ callback = version_callback,
+ help = 'Get the current installed version',
+ is_eager = True
+ ),
+
+ debug: bool = DebugOption,
+ verbose: bool = VerboseOption,
+ quiet: bool = QuietOption
+):
+ """
+ [b]Thread CLI[/b]\b\n
+ [white]Use thread from the terminal![/white]
+
+ [blue][u] [/u][/blue]
+
+ Learn more from our [link=https://github.com/python-thread/thread/blob/main/docs/command-line.md]documentation![/link]
+ """
+ verbose_args_processor(debug, verbose, quiet)
+
+
+
+# Help and Others
+@cli_base.command(rich_help_panel = 'Help and Others')
+def help():
+ """Get [yellow]help[/yellow] from the community. :question:"""
+ typer.echo('Feel free to search for or ask questions here!')
+ try:
+ logger.info('Attempting to open in web browser...')
+
+ import webbrowser
+ webbrowser.open(
+ 'https://github.com/python-thread/thread/issues',
+ new = 2
+ )
+ typer.echo('Opening in web browser!')
+
+ except Exception as e:
+ logger.warn('Failed to open web browser')
+ logger.debug(f'{e}')
+ typer.echo('https://github.com/python-thread/thread/issues')
+
+
+
+@cli_base.command(rich_help_panel = 'Help and Others')
+def docs():
+ """View our [yellow]documentation.[/yellow] :book:"""
+ typer.echo('Thanks for using Thread, here is our documentation!')
+ try:
+ logger.info('Attempting to open in web browser...')
+ import webbrowser
+ webbrowser.open(
+ 'https://github.com/python-thread/thread/blob/main/docs/command-line.md',
+ new = 2
+ )
+ typer.echo('Opening in web browser!')
+
+ except Exception as e:
+ logger.warn('Failed to open web browser')
+ logger.debug(f'{e}')
+ typer.echo('https://github.com/python-thread/thread/blob/main/docs/command-line.md')
+
+
+
+@cli_base.command(rich_help_panel = 'Help and Others')
+def report():
+ """[yellow]Report[/yellow] an issue. :bug:"""
+ typer.echo('Sorry you run into an issue, report it here!')
+ try:
+ logger.info('Attempting to open in web browser...')
+ import webbrowser
+ webbrowser.open(
+ 'https://github.com/python-thread/thread/issues',
+ new = 2
+ )
+ typer.echo('Opening in web browser!')
+
+ except Exception as e:
+ logger.warn('Failed to open web browser')
+ logger.debug(f'{e}')
+ typer.echo('https://github.com/python-thread/thread/issues')
+
+
+
+# Utils and Configs
+@cli_base.command(rich_help_panel = 'Utils and Configs')
+def config(configuration: str):
+ """
+ [blue]Configure[/blue] the system. :wrench:
+ """
+ typer.echo('Coming soon!')
\ No newline at end of file
diff --git a/src/thread-cli/process.py b/src/thread-cli/process.py
new file mode 100644
index 0000000..c75c5e7
--- /dev/null
+++ b/src/thread-cli/process.py
@@ -0,0 +1,229 @@
+"""Parallel Processing command"""
+
+import os
+import time
+import json
+import inspect
+import importlib
+
+import typer
+import logging
+from typing import Union
+
+from rich.live import Live
+from rich.panel import Panel
+from rich.console import Group
+from rich.progress import Progress, TaskID, SpinnerColumn, TextColumn, BarColumn, TimeRemainingColumn, TimeElapsedColumn
+from .utils import DebugOption, VerboseOption, QuietOption, verbose_args_processor, kwargs_processor
+
+logger = logging.getLogger('base')
+
+
+def process(
+ func: str = typer.Argument(help = '[blue].path.to.file[/blue]:[blue]function_name[/blue] OR [blue]lambda x: x[/blue]'),
+ dataset: str = typer.Argument(help = '[blue]./path/to/file.txt[/blue] OR [blue][ i for i in range(2) ][/blue]'),
+
+ args: list[str] = typer.Option([], '--arg', '-a', help = '[blue]Arguments[/blue] passed to each thread'),
+ kargs: list[str] = typer.Option([], '--kwarg', '-kw', help = '[blue]Key-Value arguments[/blue] passed to each thread'),
+ threads: int = typer.Option(8, '--threads', '-t', help = 'Maximum number of [blue]threads[/blue] (will scale down based on dataset size)'),
+
+ daemon: bool = typer.Option(False, '--daemon', '-d', help = 'Threads to run in [blue]daemon[/blue] mode'),
+ graceful_exit: bool = typer.Option(True, '--graceful-exit', '-ge', is_flag = True, help = 'Whether to [blue]gracefully exit[/blue] on abrupt exit (etc. CTRL+C)'),
+ output: str = typer.Option('./output.json', '--output', '-o', help = '[blue]Output[/blue] file location'),
+ fileout: bool = typer.Option(True, '--fileout', is_flag = True, help = 'Whether to [blue]write[/blue] output to a file'),
+ stdout: bool = typer.Option(False, '--stdout', is_flag = True, help = 'Whether to [blue]print[/blue] the output'),
+
+ debug: bool = DebugOption,
+ verbose: bool = VerboseOption,
+ quiet: bool = QuietOption
+):
+ """
+ [bold]Utilise parallel processing on a dataset[/bold]
+
+ \b\n
+ [bold white]:glowing_star: Important[/bold white]
+ Args and Kwargs can be parsed by adding multiple -a or -kw
+
+ [green]$ thread[/green] [blue]process[/blue] ... -a 'an arg' -kw myKey=myValue -arg testing --kwarg a1=a2
+ [white]=> args = [ [green]'an arg'[/green], [green]'testing'[/green] ][/white]
+ [white] kwargs = { [green]'myKey'[/green]: [green]'myValue'[/green], [green]'a1'[/green]: [green]'a2'[/green] }[/white]
+
+ [blue][u] [/u][/blue]
+
+ Learn more from our [link=https://github.com/python-thread/thread/blob/main/docs/command-line.md#parallel-processing-thread-process]documentation![/link]
+ """
+ verbose_args_processor(debug, verbose, quiet)
+ kwargs = kwargs_processor(kargs)
+ logger.debug('Processed kwargs: %s' % kwargs)
+
+
+ # Verify output
+ if not fileout and not stdout:
+ raise typer.BadParameter('No output method specified')
+
+ if fileout and not os.path.exists('/'.join(output.split('/')[:-1])):
+ raise typer.BadParameter('Output file directory does not exist')
+
+
+
+
+ # Loading function
+ f = None
+ try:
+ logger.info('Attempted to interpret function')
+ f = eval(func) # I know eval is bad practice, but I have yet to find a safer replacement
+ logger.debug('Evaluated function: %s' % f)
+
+ if not inspect.isfunction(f):
+ logger.info('Invalid function')
+ except Exception:
+ logger.info('Failed to interpret function')
+
+ if not f:
+ try:
+ logger.info('Attempting to fetch function file')
+
+ fPath, fName = func.split(':')
+ f = importlib.import_module(fPath).__dict__[fName]
+ logger.debug('Evaluated function: %s' % f)
+
+ if not inspect.isfunction(f):
+ logger.info('Not a function')
+ raise Exception('Not a function')
+ except Exception as e:
+ logger.warning('Failed to fetch function')
+ raise typer.BadParameter('Failed to fetch function') from e
+
+
+
+
+ # Loading dataset
+ ds: Union[list, tuple, set, None] = None
+ try:
+ logger.info('Attempting to interpret dataset')
+ ds = eval(dataset)
+ logger.debug('Evaluated dataset: %s' % (str(ds)[:125] + '...' if len(str(ds)) > 125 else ds))
+
+ if not isinstance(ds, (list, tuple, set)):
+ logger.info('Invalid dataset literal')
+ ds = None
+
+ except Exception:
+ logger.info('Failed to interpret dataset')
+
+ if not ds:
+ try:
+ logger.info('Attempting to fetch data file')
+ if not os.path.isfile(dataset):
+ logger.info('Invalid file path')
+ raise Exception('Invalid file path')
+
+ with open(dataset, 'r') as a:
+ ds = [ i.endswith('\n') and i[:-2] for i in a.readlines() ]
+ except Exception as e:
+ logger.warning('Failed to read dataset')
+ raise typer.BadParameter('Failed to read dataset') from e
+
+ logger.info('Interpreted dataset')
+
+
+ # Setup
+ logger.debug('Importing module')
+ from thread import Settings, ParallelProcessing
+ logger.info('Spawning threads... [Expected: {tcount} threads]'.format(tcount=min(len(ds), threads)))
+
+ Settings.set_graceful_exit(graceful_exit)
+ newProcess = ParallelProcessing(
+ function = f,
+ dataset = list(ds),
+ args = args,
+ kwargs = kwargs,
+ daemon = daemon,
+ max_threads = threads
+ )
+
+ logger.info('Created parallel process')
+ logger.info('Starting parallel process')
+
+ start_t = time.perf_counter()
+ newProcess.start()
+
+ logger.info('Started parallel processes')
+ typer.echo('Waiting for parallel processes to complete, this may take a while...')
+
+
+ # Progress bar :D
+ threadCount = len(newProcess._threads)
+
+ thread_progress = Progress(
+ SpinnerColumn(),
+ TextColumn('{task.description}'),
+ '•',
+ TimeRemainingColumn(),
+ BarColumn(bar_width = 80),
+ TextColumn('{task.percentage:>3.1f}%')
+ )
+ overall_progress = Progress(
+ TimeElapsedColumn(),
+ BarColumn(bar_width = 110),
+ TextColumn('{task.description}')
+ )
+
+ workerjobs: list[TaskID] = [
+ thread_progress.add_task(
+ f'[bold blue][T {threadNum}]',
+ total = 100
+ )
+ for threadNum in range(threadCount)
+ ]
+ overalljob = overall_progress.add_task('(0 of ?)', total = 100)
+
+
+ with Live(
+ Group(
+ Panel(thread_progress),
+ overall_progress,
+ ),
+ refresh_per_second = 10
+ ):
+ completed = 0
+ while completed != threadCount:
+ i = 0
+ completed = 0
+ progressAvg = 0
+
+ for jobID in workerjobs:
+ jobProgress = newProcess._threads[i].progress
+ thread_progress.update(jobID, completed = round(jobProgress * 100, 2))
+ if jobProgress == 1:
+ thread_progress.stop_task(jobID)
+ thread_progress.update(jobID, description = '[bold green]Completed')
+ completed += 1
+
+ progressAvg += jobProgress
+ i += 1
+
+ # Update overall
+ overall_progress.update(
+ overalljob,
+ description = f'[bold {"green" if completed == threadCount else "#AAAAAA"}]({completed} of {threadCount})',
+ completed = round((progressAvg / threadCount) * 100, 2)
+ )
+ time.sleep(0.1)
+
+
+ result = newProcess.get_return_values()
+
+ typer.echo(f'Completed in {(time.perf_counter() - start_t):.5f}s')
+ if fileout:
+ typer.echo(f'Writing to {output}')
+ try:
+ with open(output, 'w') as f:
+ json.dump(result, f, indent = 2)
+ logger.info('Wrote to file')
+ except Exception as e:
+ logger.error('Failed to write to file')
+ logger.debug(str(e))
+
+ if stdout:
+ typer.echo(result)
\ No newline at end of file
diff --git a/src/thread-cli/py.typed b/src/thread-cli/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/src/thread-cli/utils/__init__.py b/src/thread-cli/utils/__init__.py
new file mode 100644
index 0000000..4462f58
--- /dev/null
+++ b/src/thread-cli/utils/__init__.py
@@ -0,0 +1,9 @@
+from . import logging
+from .processors import (
+ verbose_args_processor,
+ kwargs_processor,
+
+ DebugOption,
+ VerboseOption,
+ QuietOption
+)
diff --git a/src/thread-cli/utils/logging.py b/src/thread-cli/utils/logging.py
new file mode 100644
index 0000000..f6da703
--- /dev/null
+++ b/src/thread-cli/utils/logging.py
@@ -0,0 +1,33 @@
+import logging
+from colorama import init, Fore, Style
+
+init(autoreset=True)
+
+
+# Stdout color configuration
+class ColorFormatter(logging.Formatter):
+ COLORS = {
+ "DEBUG": Fore.BLUE,
+ "INFO": Fore.GREEN,
+ "WARNING": Fore.YELLOW,
+ "ERROR": Fore.RED,
+ "CRITICAL": Fore.RED + Style.BRIGHT,
+ }
+
+ def format(self, record):
+ color = self.COLORS.get(record.levelname, "")
+ record.levelname = record.levelname if not color else color + Style.BRIGHT + f"{record.levelname:<9}|"
+ record.msg = record.msg if not color else color + Fore.WHITE + Style.NORMAL + record.msg
+
+ return logging.Formatter.format(self, record)
+
+
+# Configure logger
+class ColorLogger(logging.Logger):
+ def __init__(self, name):
+ logging.Logger.__init__(self, name, logging.DEBUG)
+ colorFormatter = ColorFormatter("%(levelname)s %(message)s" + Style.RESET_ALL)
+
+ console = logging.StreamHandler()
+ console.setFormatter(colorFormatter)
+ self.addHandler(console)
diff --git a/src/thread-cli/utils/processors.py b/src/thread-cli/utils/processors.py
new file mode 100644
index 0000000..7356eb2
--- /dev/null
+++ b/src/thread-cli/utils/processors.py
@@ -0,0 +1,48 @@
+# Verbose Command Processor #
+import typer
+import logging
+
+
+# Verbose Options #
+DebugOption = typer.Option(
+ False, '--debug',
+ help = 'Set verbosity level to [blue]DEBUG[/blue]',
+ is_flag = True
+)
+VerboseOption = typer.Option(
+ False, '--verbose', '-v',
+ help = 'Set verbosity level to [green]INFO[/green]',
+ is_flag = True
+)
+QuietOption = typer.Option(
+ False, '--quiet', '-q',
+ help = 'Set verbosity level to [red]ERROR[/red]',
+ is_flag = True
+)
+
+
+# Helper functions #
+
+
+# Processors #
+def verbose_args_processor(debug: bool, verbose: bool, quiet: bool):
+ """Handles setting and raising exceptions for verbose"""
+ if verbose and quiet:
+ raise typer.BadParameter('--quiet cannot be used with --verbose')
+
+ if verbose and debug:
+ raise typer.BadParameter('--debug cannot be used with --verbose')
+
+ logging.getLogger('base').setLevel((
+ (debug and logging.DEBUG) or
+ (verbose and logging.INFO) or
+ logging.ERROR
+ ))
+
+def kwargs_processor(arguments: list[str]) -> dict[str, str]:
+ """Processes arguments into kwargs"""
+ return {
+ kwarg[0]: kwarg[1]
+ for i in arguments
+ if (kwarg := i.split('='))
+ }
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_placeholder.py b/tests/test_placeholder.py
new file mode 100644
index 0000000..d20e63f
--- /dev/null
+++ b/tests/test_placeholder.py
@@ -0,0 +1,4 @@
+def test_packagesExist():
+ import thread
+
+ assert thread