Compare commits

...

10 Commits

17 changed files with 2748 additions and 110 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
__pycache__/
.env

49
README.md Normal file
View File

@@ -0,0 +1,49 @@
# Purpose
To keep track of my vitamins, minerals, supplements and prescriptions so I can know when to order more, and to have enough when I do my monthly 'packaging' of them into easy to use containers.
# Inventory CLI
```bash
> sup status
The next fill-up is on <date>, and you won't have enough of
- x (need quantity x')
- y (need quantity y')
- z (need quantity z')
> sup fill
Okay, next fill-up set to <date X days from now> (configured in `sup.toml`::FILL_EVERY_X_DAYS)
# (this is to add to the inventory; any changes to consumption should be done in `supps.toml`)
> sup add
>> name? <name>
>> date? (today)
>> number of bottles? (1)
>> quantity?
>> serving quantity?
>> serving unit? (mg)
```
# Configuration
TODO: talk about product_aliases
To configure what you take, how much, and when, add entries to `supps.toml`.
The fields are configured as such in the `Supp` class:
```python
name: str
units: t.Literal["caps", "mg", "g", "ml", "mcg", "iu"] = "mg"
# these are meant to be doses; units are defined below
morning: int | float = 0
lunch: int | float = 0
dinner: int | float = 0
bedtime: int | float = 0
days_per_week: int = 7
winter_only: bool = False
```

560
inventory.json Normal file
View File

@@ -0,0 +1,560 @@
[
{
"orderDate": "2023-10-14:00:00.000Z",
"name": "Ultimate Omega, Lemon",
"quantity": 180,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 640,
"numBottles": 1
},
{
"orderDate": "2023-10-14:00:00.000Z",
"name": "Super K",
"quantity": 90,
"quantityUnits": "caps",
"servingUnit": "mcg",
"numUnitsInServing": 1500,
"numBottles": 1
},
{
"orderDate": "2022-09-19:00:00.000Z",
"name": "High Absorption CoQ10 with BioPerine",
"quantity": 60,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 120,
"numBottles": 2
},
{
"orderDate": "2024-01-30T23:00:00.000Z",
"name": "NAC N-Acetyl Cysteine",
"quantity": 60,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 1000,
"numBottles": 4
},
{
"orderDate": "2024-01-02:00:00.000Z",
"name": "Glucosamine Sulfate",
"quantity": 240,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 750,
"numBottles": 1
},
{
"orderDate": "2024-01-02:00:00.000Z",
"name": "Vitamin K2, MK-4 (Menatetrenone)",
"quantity": 180,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 5,
"numBottles": 1
},
{
"orderDate": "2023-07-03:00:00.000Z",
"name": "Glycine",
"quantity": 250,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 350,
"numBottles": 1
},
{
"orderDate": "2024-01-23T23:00:00.000Z",
"name": "Fisetin Novusetin",
"quantity": 30,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 100,
"numBottles": 4
},
{
"orderDate": "2024-01-23T23:00:00.000Z",
"name": "MK-7 Vitamin K-2",
"quantity": 60,
"quantityUnits": "caps",
"servingUnit": "mcg",
"numUnitsInServing": 300,
"numBottles": 4
},
{
"orderDate": "2024-01-23T23:00:00.000Z",
"name": "Liquid D-3 & MK-7",
"quantity": 82,
"quantityUnits": "iu",
"servingUnit": "iu",
"numUnitsInServing": 5000,
"numBottles": 1
},
{
"orderDate": "2024-01-23T23:00:00.000Z",
"name": "Calcium AKG Longevity",
"quantity": 500,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 1000,
"numBottles": 4
},
{
"orderDate": "2024-01-23T23:00:00.000Z",
"name": "Magtein",
"quantity": 180,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 667,
"numBottles": 1
},
{
"orderDate": "2024-01-23T23:00:00.000Z",
"name": "Magnesium Taurate",
"quantity": 180,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 125,
"numBottles": 1
},
{
"orderDate": "2024-01-23T23:00:00.000Z",
"name": "Collagen",
"quantity": 290,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 6000,
"numBottles": 1
},
{
"orderDate": "2024-01-23T23:00:00.000Z",
"name": "Ginger Root",
"quantity": 100,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 550,
"numBottles": 1
},
{
"orderDate": "2024-01-09T23:00:00.000Z",
"name": "PQQ",
"quantity": 60,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 20,
"numBottles": 1
},
{
"orderDate": "2023-08-02T23:00:00.000Z",
"name": "B-50",
"quantity": 250,
"quantityUnits": "caps",
"servingUnit": "caps",
"numUnitsInServing": 1,
"numBottles": 1
},
{
"orderDate": "2024-01-09T23:00:00.000Z",
"name": "Magnesium Taurate +",
"quantity": 200,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 400,
"numBottles": 1
},
{
"orderDate": "2024-01-09T23:00:00.000Z",
"name": "High Absorption Magnesium Glycinate 350",
"quantity": 160,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 350,
"numBottles": 1
},
{
"orderDate": "2024-01-05T23:00:00.000Z",
"name": "Ginger Root",
"quantity": 100,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 550,
"numBottles": 1
},
{
"orderDate": "2024-01-05T23:00:00.000Z",
"name": "Probiotic-10",
"quantity": 50,
"quantityUnits": "caps",
"servingUnit": "caps",
"numUnitsInServing": 1,
"numBottles": 1
},
{
"orderDate": "2024-01-05T23:00:00.000Z",
"name": "Iron Bisglycinate",
"quantity": 60,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 25,
"numBottles": 1
},
{
"orderDate": "2024-01-05T23:00:00.000Z",
"name": "Optimized Folate",
"quantity": 100,
"quantityUnits": "caps",
"servingUnit": "mcg",
"numUnitsInServing": 1700,
"numBottles": 1
},
{
"orderDate": "2024-01-05T23:00:00.000Z",
"name": "Extend-Release Magnesium",
"quantity": 60,
"quantityUnits": "caps",
"servingUnit": "caps",
"numUnitsInServing": 1,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Fisetin with Novusetin",
"quantity": 30,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 100,
"numBottles": 2
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Genistein from Sophora Japonica",
"quantity": 60,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 125,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Aged Garlic Extract",
"quantity": 300,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 600,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Crucera-SGS",
"quantity": 60,
"quantityUnits": "caps",
"servingUnit": "caps",
"numUnitsInServing": 1,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Double Strength Taurine",
"quantity": 250,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 1000,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Ashwagandha",
"quantity": 120,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 300,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Astaxanthin",
"quantity": 120,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 12,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Methyl B-12",
"quantity": 100,
"quantityUnits": "caps",
"servingUnit": "mcg",
"numUnitsInServing": 1000,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Boron",
"quantity": 250,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 3,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Calcium AKG Longevity",
"quantity": 500,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 1000,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Organic Turmeric Curcumin",
"quantity": 180,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 2250,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Lutein & Zeaxanthin",
"quantity": 60,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 20,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Zinc Citrate",
"quantity": 90,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 15,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "NAD+ Cell Regenerator",
"quantity": 30,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 300,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Creatine",
"quantity": 450,
"quantityUnits": "g",
"servingUnit": "g",
"numUnitsInServing": 5,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Lycopene",
"quantity": 120,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 10,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Vitamin C",
"quantity": 250,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 500,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Natural Vitamin K2 MK-7 with MenaQ7",
"quantity": 60,
"quantityUnits": "caps",
"servingUnit": "mcg",
"numUnitsInServing": 100,
"numBottles": 4
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "L-Lysine",
"quantity": 250,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 1000,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "L-Tyrosine",
"quantity": 120,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 500,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Lithium Orotate Drops",
"quantity": 59,
"quantityUnits": "ml",
"servingUnit": "mcg",
"numUnitsInServing": 1000,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Liquid Iodine Plus",
"quantity": 59,
"quantityUnits": "ml",
"servingUnit": "ml",
"numUnitsInServing": 126,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Hyaluronic Acid",
"quantity": 120,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 100,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Aspirin",
"quantity": 300,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 81,
"numBottles": 1
},
{
"orderDate": "2024-01-04T23:00:00.000Z",
"name": "Naturally Sourced Vitamin E",
"quantity": 100,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 134,
"numBottles": 1
},
{
"name": "DHEA",
"quantity": 90,
"numUnitsInServing": 25,
"servingUnit": "mg",
"quantityUnits": "caps",
"orderDate": "2024-02-03",
"numBottles": 1
},
{
"orderDate": "2024-02-04T23:00:00.000Z",
"name": "Ashwagandha",
"quantity": 120,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 300,
"numBottles": 1
},
{
"orderDate": "2024-02-04T23:00:00.000Z",
"name": "Crucera-SGS",
"quantity": 60,
"quantityUnits": "caps",
"servingUnit": "caps",
"numUnitsInServing": 1,
"numBottles": 1
},
{
"orderDate": "2024-02-04T23:00:00.000Z",
"name": "High Absorption CoQ10 with BioPerine",
"quantity": 120,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 100,
"numBottles": 1
},
{
"orderDate": "2024-02-04T23:00:00.000Z",
"name": "Ultimate Omega",
"quantity": 640,
"quantityUnits": "mg",
"servingUnit": "mg",
"numUnitsInServing": 1280,
"numBottles": 1
},
{
"orderDate": "2024-02-04T23:00:00.000Z",
"name": "Genistein from Sophora Japonica",
"quantity": 60,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 125,
"numBottles": 1
},
{
"orderDate": "2024-02-04T23:00:00.000Z",
"name": "Ginger Root",
"quantity": 100,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 550,
"numBottles": 3
},
{
"orderDate": "2024-02-04T23:00:00.000Z",
"name": "Glucosamine Sulfate",
"quantity": 240,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 750,
"numBottles": 1
},
{
"orderDate": "2024-02-04T23:00:00.000Z",
"name": "Glycine",
"quantity": 250,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 1000,
"numBottles": 1
},
{
"orderDate": "2024-02-04T23:00:00.000Z",
"name": "Liquid Iodine Plus",
"quantity": 59,
"quantityUnits": "mcl",
"servingUnit": "mcl",
"numUnitsInServing": 126,
"numBottles": 1
},
{
"orderDate": "2024-02-04T23:00:00.000Z",
"name": "Super K",
"quantity": 90,
"quantityUnits": "caps",
"servingUnit": "mcg",
"numUnitsInServing": 1500,
"numBottles": 1
},
{
"orderDate": "2024-02-04T23:00:00.000Z",
"name": "Lithium",
"quantity": 100,
"quantityUnits": "caps",
"servingUnit": "mcg",
"numUnitsInServing": 1000,
"numBottles": 1
},
{
"orderDate": "2024-02-04T23:00:00.000Z",
"name": "Hyaluronic Acid",
"quantity": 30,
"quantityUnits": "caps",
"servingUnit": "mg",
"numUnitsInServing": 200,
"numBottles": 2
}
]

930
poetry.lock generated Normal file
View File

@@ -0,0 +1,930 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "annotated-types"
version = "0.6.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
files = [
{file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"},
{file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
]
[[package]]
name = "astroid"
version = "3.0.2"
description = "An abstract syntax tree for Python with inference support."
optional = false
python-versions = ">=3.8.0"
files = [
{file = "astroid-3.0.2-py3-none-any.whl", hash = "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c"},
{file = "astroid-3.0.2.tar.gz", hash = "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91"},
]
[[package]]
name = "asttokens"
version = "2.4.1"
description = "Annotate AST trees with source code positions"
optional = false
python-versions = "*"
files = [
{file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"},
{file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"},
]
[package.dependencies]
six = ">=1.12.0"
[package.extras]
astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"]
test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"]
[[package]]
name = "black"
version = "24.1.1"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"},
{file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"},
{file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"},
{file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"},
{file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"},
{file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"},
{file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"},
{file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"},
{file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"},
{file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"},
{file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"},
{file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"},
{file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"},
{file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"},
{file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"},
{file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"},
{file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"},
{file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"},
{file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"},
{file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"},
{file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"},
{file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[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 = "decorator"
version = "5.1.1"
description = "Decorators for Humans"
optional = false
python-versions = ">=3.5"
files = [
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
]
[[package]]
name = "dill"
version = "0.3.8"
description = "serialize all of Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"},
{file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"},
]
[package.extras]
graph = ["objgraph (>=1.7.2)"]
profile = ["gprof2dot (>=2022.7.29)"]
[[package]]
name = "executing"
version = "2.0.1"
description = "Get the currently executing AST node of a frame, and other information"
optional = false
python-versions = ">=3.5"
files = [
{file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"},
{file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"},
]
[package.extras]
tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"]
[[package]]
name = "greenlet"
version = "3.0.3"
description = "Lightweight in-process concurrent programming"
optional = false
python-versions = ">=3.7"
files = [
{file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"},
{file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"},
{file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"},
{file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"},
{file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"},
{file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"},
{file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"},
{file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"},
{file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"},
{file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"},
{file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"},
{file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"},
{file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"},
{file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"},
{file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"},
{file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"},
{file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"},
{file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"},
{file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"},
{file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"},
{file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"},
{file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"},
{file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"},
{file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"},
{file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"},
{file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"},
{file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"},
{file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"},
{file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"},
{file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"},
{file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"},
{file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"},
{file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"},
{file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"},
{file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"},
{file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"},
{file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"},
{file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"},
{file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"},
{file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"},
{file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"},
{file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"},
{file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"},
{file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"},
{file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"},
{file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"},
{file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"},
{file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"},
{file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"},
{file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"},
{file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"},
{file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"},
{file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"},
{file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"},
{file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"},
{file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"},
{file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"},
{file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"},
]
[package.extras]
docs = ["Sphinx", "furo"]
test = ["objgraph", "psutil"]
[[package]]
name = "ipython"
version = "8.21.0"
description = "IPython: Productive Interactive Computing"
optional = false
python-versions = ">=3.10"
files = [
{file = "ipython-8.21.0-py3-none-any.whl", hash = "sha256:1050a3ab8473488d7eee163796b02e511d0735cf43a04ba2a8348bd0f2eaf8a5"},
{file = "ipython-8.21.0.tar.gz", hash = "sha256:48fbc236fbe0e138b88773fa0437751f14c3645fb483f1d4c5dee58b37e5ce73"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
decorator = "*"
jedi = ">=0.16"
matplotlib-inline = "*"
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
prompt-toolkit = ">=3.0.41,<3.1.0"
pygments = ">=2.4.0"
stack-data = "*"
traitlets = ">=5"
[package.extras]
all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.23)", "pandas", "pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"]
black = ["black"]
doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"]
kernel = ["ipykernel"]
nbconvert = ["nbconvert"]
nbformat = ["nbformat"]
notebook = ["ipywidgets", "notebook"]
parallel = ["ipyparallel"]
qtconsole = ["qtconsole"]
test = ["pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "testpath"]
test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "testpath", "trio"]
[[package]]
name = "isort"
version = "5.13.2"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.8.0"
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 = "jedi"
version = "0.19.1"
description = "An autocompletion tool for Python that can be used for text editors."
optional = false
python-versions = ">=3.6"
files = [
{file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"},
{file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"},
]
[package.dependencies]
parso = ">=0.8.3,<0.9.0"
[package.extras]
docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
[[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 = "matplotlib-inline"
version = "0.1.6"
description = "Inline Matplotlib backend for Jupyter"
optional = false
python-versions = ">=3.5"
files = [
{file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"},
{file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"},
]
[package.dependencies]
traitlets = "*"
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "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 = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "mysql-connector-python"
version = "8.3.0"
description = "MySQL driver written in Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "mysql-connector-python-8.3.0.tar.gz", hash = "sha256:e4ff23aa8036b4c5b6463fa81398bb5a528a29f99955de6ba937f0bba57a2fe3"},
{file = "mysql_connector_python-8.3.0-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:f4ee7e07cca6b744874d60d6b0b24817d9246eb4e8d7269b7ddbe68763a0bd13"},
{file = "mysql_connector_python-8.3.0-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:5718e426cf67f041772d4984f709052201883f74190ba6feaddce5cbd3b99e6f"},
{file = "mysql_connector_python-8.3.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:0deb38f05057e12af091a48e03a1ff00e213945880000f802879fae5665e7502"},
{file = "mysql_connector_python-8.3.0-cp310-cp310-manylinux_2_17_x86_64.whl", hash = "sha256:4be4165e4cd5acb4659261ddc74e9164d2dfa0d795d5695d52f2bf39ea0762fa"},
{file = "mysql_connector_python-8.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:51d97bf771519829797556718d81e8b9bdcd0a00427740ca57c085094c8bde17"},
{file = "mysql_connector_python-8.3.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:5e2c86c60be08c71bae755d811fe8b89ec4feb8117ec3440ebc6c042dd6f06bc"},
{file = "mysql_connector_python-8.3.0-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:de74055944b214bff56e1752ec213d705c421414c67a250fb695af0c5c214135"},
{file = "mysql_connector_python-8.3.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:b2901391b651d60dab3cc8985df94976fc1ea59fa7324c5b19d0a4177914c8dd"},
{file = "mysql_connector_python-8.3.0-cp311-cp311-manylinux_2_17_x86_64.whl", hash = "sha256:55cb57d8098c721abce20fdef23232663977c0e5c87a4d0f9f73466f32c7d168"},
{file = "mysql_connector_python-8.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:201e609159b84a247be87b76f5deb79e8c6b368e91f043790e62077f13f3fed8"},
{file = "mysql_connector_python-8.3.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:c57d02fd6c28be444487e7905ede09e3fecb18377cf82908ca262826369d3401"},
{file = "mysql_connector_python-8.3.0-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:9302d774025e76a0fac46bfeea8854b3d6819715a6a16ff23bfcda04218a76b7"},
{file = "mysql_connector_python-8.3.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:85fa878fdd6accaeb7d609bd2637c2cfa61592e7f9bdbdc0da18b2fa998d3d5a"},
{file = "mysql_connector_python-8.3.0-cp312-cp312-manylinux_2_17_x86_64.whl", hash = "sha256:de0f2f2baa9e091ca8bdc4a091f874f9cd0b84b256389596adb0e032a05fe9f9"},
{file = "mysql_connector_python-8.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:27f8be2087627366a44a6831ec68b568c98dbf0f4ceff24682d90c21db6e0f1f"},
{file = "mysql_connector_python-8.3.0-cp38-cp38-macosx_13_0_x86_64.whl", hash = "sha256:ec6dc3434a7deef74ab04e8978f6c5e181866a5423006c1b5aec5390a189d28d"},
{file = "mysql_connector_python-8.3.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:73ee8bc5f9626c42b37342a91a825cddb3461f6bfbbd6524d8ccfd3293aaa088"},
{file = "mysql_connector_python-8.3.0-cp38-cp38-manylinux_2_17_x86_64.whl", hash = "sha256:1db5b48b4ff7d24344217ed2418b162c7677eec86ab9766dc0e5feae39c90974"},
{file = "mysql_connector_python-8.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:77bae496566d3da77bb0e938d89243103d20ee41633f626a47785470451bf45c"},
{file = "mysql_connector_python-8.3.0-cp39-cp39-macosx_13_0_arm64.whl", hash = "sha256:f7acacdf9fd4260702f360c00952ad9a9cc73e8b7475e0d0c973c085a3dd7b7d"},
{file = "mysql_connector_python-8.3.0-cp39-cp39-macosx_13_0_x86_64.whl", hash = "sha256:5f707a9b040ad4700fc447ba955c78b08f2dd5affde37ac2401918f7b6daaba3"},
{file = "mysql_connector_python-8.3.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:125714c998a697592bc56cce918a1acc58fadc510a7f588dbef3e53a1920e086"},
{file = "mysql_connector_python-8.3.0-cp39-cp39-manylinux_2_17_x86_64.whl", hash = "sha256:7f4f5fa844c19ee3a78c4606f6e138b06829e75469592d90246a290c7befc322"},
{file = "mysql_connector_python-8.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:de5c3ee89d9276356f93df003949d3ba4c486f32fec9ec9fd7bc0caab124d89c"},
{file = "mysql_connector_python-8.3.0-py2.py3-none-any.whl", hash = "sha256:e868ccc7ad9fbc242546db04673d89cee87d12b8139affd114524553df4e5d6a"},
]
[package.extras]
dns-srv = ["dnspython (>=1.16.0,<=2.3.0)"]
fido2 = ["fido2 (==1.1.2)"]
gssapi = ["gssapi (>=1.6.9,<=1.8.2)"]
opentelemetry = ["Deprecated (>=1.2.6)", "typing-extensions (>=3.7.4)", "zipp (>=0.5)"]
[[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 = "parso"
version = "0.8.3"
description = "A Python Parser"
optional = false
python-versions = ">=3.6"
files = [
{file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
{file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
]
[package.extras]
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
testing = ["docopt", "pytest (<6.0.0)"]
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
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 = "pexpect"
version = "4.9.0"
description = "Pexpect allows easy control of interactive console applications."
optional = false
python-versions = "*"
files = [
{file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
{file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"},
]
[package.dependencies]
ptyprocess = ">=0.5"
[[package]]
name = "platformdirs"
version = "4.2.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"},
{file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
[[package]]
name = "prompt-toolkit"
version = "3.0.43"
description = "Library for building powerful interactive command lines in Python"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"},
{file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"},
]
[package.dependencies]
wcwidth = "*"
[[package]]
name = "ptyprocess"
version = "0.7.0"
description = "Run a subprocess in a pseudo terminal"
optional = false
python-versions = "*"
files = [
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
]
[[package]]
name = "pure-eval"
version = "0.2.2"
description = "Safely evaluate AST nodes without side effects"
optional = false
python-versions = "*"
files = [
{file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
{file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
]
[package.extras]
tests = ["pytest"]
[[package]]
name = "pydantic"
version = "2.6.1"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"},
{file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"},
]
[package.dependencies]
annotated-types = ">=0.4.0"
pydantic-core = "2.16.2"
typing-extensions = ">=4.6.1"
[package.extras]
email = ["email-validator (>=2.0.0)"]
[[package]]
name = "pydantic-core"
version = "2.16.2"
description = ""
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"},
{file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"},
{file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"},
{file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"},
{file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"},
{file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"},
{file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef"},
{file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"},
{file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"},
{file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"},
{file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"},
{file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"},
{file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"},
{file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"},
{file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"},
{file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"},
{file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"},
{file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"},
{file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272"},
{file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"},
{file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"},
{file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"},
{file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"},
{file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"},
{file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"},
{file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"},
{file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"},
{file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"},
{file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"},
{file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"},
{file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"},
{file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380"},
{file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"},
{file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"},
{file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"},
{file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"},
{file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"},
{file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"},
{file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"},
{file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"},
{file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"},
{file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"},
{file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"},
{file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"},
{file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf"},
{file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"},
{file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"},
{file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"},
{file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"},
{file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"},
{file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"},
{file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"},
{file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"},
{file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"},
{file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"},
{file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"},
{file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878"},
{file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"},
{file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"},
{file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"},
{file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"},
{file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"},
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"},
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"},
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"},
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d"},
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"},
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"},
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"},
{file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"},
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"},
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"},
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"},
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20"},
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"},
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"},
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"},
{file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"},
{file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"},
]
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[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 = "pylint"
version = "3.0.3"
description = "python code static checker"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "pylint-3.0.3-py3-none-any.whl", hash = "sha256:7a1585285aefc5165db81083c3e06363a27448f6b467b3b0f30dbd0ac1f73810"},
{file = "pylint-3.0.3.tar.gz", hash = "sha256:58c2398b0301e049609a8429789ec6edf3aabe9b6c5fec916acd18639c16de8b"},
]
[package.dependencies]
astroid = ">=3.0.1,<=3.1.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""}
isort = ">=4.2.5,<5.13.0 || >5.13.0,<6"
mccabe = ">=0.6,<0.8"
platformdirs = ">=2.2.0"
tomlkit = ">=0.10.1"
[package.extras]
spelling = ["pyenchant (>=3.2,<4.0)"]
testutils = ["gitpython (>3)"]
[[package]]
name = "python-dotenv"
version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
files = [
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
]
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "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 = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "sqlalchemy"
version = "2.0.25"
description = "Database Abstraction Library"
optional = false
python-versions = ">=3.7"
files = [
{file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84daa0a2055df9ca0f148a64fdde12ac635e30edbca80e87df9b3aaf419e144a"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5693145220517b5f42393e07a6898acdfe820e136c98663b971906120549da5"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-win32.whl", hash = "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-win_amd64.whl", hash = "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa9373708763ef46782d10e950b49d0235bfe58facebd76917d3f5cbf5971aed"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75432b5b14dc2fff43c50435e248b45c7cdadef73388e5610852b95280ffd0e9"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-win32.whl", hash = "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a21853f5daeb50412d459cfb13cb82c089ad4c04ec208cd14dddd99fc23b39"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b64b183d610b424a160b0d4d880995e935208fc043d0302dd29fee32d1ee3f95"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-win32.whl", hash = "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-win_amd64.whl", hash = "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bb209a73b8307f8fe4fe46f6ad5979649be01607f11af1eb94aa9e8a3aaf77f0"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798f717ae7c806d67145f6ae94dc7c342d3222d3b9a311a784f371a4333212c7"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd402169aa00df3142149940b3bf9ce7dde075928c1886d9a1df63d4b8de62"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0d3cab3076af2e4aa5693f89622bef7fa770c6fec967143e4da7508b3dceb9b9"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:74b080c897563f81062b74e44f5a72fa44c2b373741a9ade701d5f789a10ba23"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-win32.whl", hash = "sha256:87d91043ea0dc65ee583026cb18e1b458d8ec5fc0a93637126b5fc0bc3ea68c4"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-win_amd64.whl", hash = "sha256:75f99202324383d613ddd1f7455ac908dca9c2dd729ec8584c9541dd41822a2c"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3be4987e3ee9d9a380b66393b77a4cd6d742480c951a1c56a23c335caca4ce3"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8b8cb63d3ea63b29074dcd29da4dc6a97ad1349151f2d2949495418fd6e48db9"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-win32.whl", hash = "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-win_amd64.whl", hash = "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecf6d4cda1f9f6cb0b45803a01ea7f034e2f1aed9475e883410812d9f9e3cfcf"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:790f533fa5c8901a62b6fef5811d48980adeb2f51f1290ade8b5e7ba990ba3de"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-win32.whl", hash = "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-win_amd64.whl", hash = "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7"},
{file = "SQLAlchemy-2.0.25-py3-none-any.whl", hash = "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3"},
{file = "SQLAlchemy-2.0.25.tar.gz", hash = "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08"},
]
[package.dependencies]
greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
typing-extensions = ">=4.6.0"
[package.extras]
aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
aioodbc = ["aioodbc", "greenlet (!=0.4.17)"]
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
asyncio = ["greenlet (!=0.4.17)"]
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"]
mssql = ["pyodbc"]
mssql-pymssql = ["pymssql"]
mssql-pyodbc = ["pyodbc"]
mypy = ["mypy (>=0.910)"]
mysql = ["mysqlclient (>=1.4.0)"]
mysql-connector = ["mysql-connector-python"]
oracle = ["cx_oracle (>=8)"]
oracle-oracledb = ["oracledb (>=1.0.1)"]
postgresql = ["psycopg2 (>=2.7)"]
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
postgresql-psycopg = ["psycopg (>=3.0.7)"]
postgresql-psycopg2binary = ["psycopg2-binary"]
postgresql-psycopg2cffi = ["psycopg2cffi"]
postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
pymysql = ["pymysql"]
sqlcipher = ["sqlcipher3_binary"]
[[package]]
name = "sqlmodel"
version = "0.0.14"
description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness."
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "sqlmodel-0.0.14-py3-none-any.whl", hash = "sha256:accea3ff5d878e41ac439b11e78613ed61ce300cfcb860e87a2d73d4884cbee4"},
{file = "sqlmodel-0.0.14.tar.gz", hash = "sha256:0bff8fc94af86b44925aa813f56cf6aabdd7f156b73259f2f60692c6a64ac90e"},
]
[package.dependencies]
pydantic = ">=1.10.13,<3.0.0"
SQLAlchemy = ">=2.0.0,<2.1.0"
[[package]]
name = "stack-data"
version = "0.6.3"
description = "Extract data from python stack frames and tracebacks for informative displays"
optional = false
python-versions = "*"
files = [
{file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
{file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
]
[package.dependencies]
asttokens = ">=2.1.0"
executing = ">=1.2.0"
pure-eval = "*"
[package.extras]
tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
[[package]]
name = "tomlkit"
version = "0.12.3"
description = "Style preserving TOML library"
optional = false
python-versions = ">=3.7"
files = [
{file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"},
{file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"},
]
[[package]]
name = "traitlets"
version = "5.14.1"
description = "Traitlets Python configuration system"
optional = false
python-versions = ">=3.8"
files = [
{file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"},
{file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"},
]
[package.extras]
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"]
[[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"
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"},
]
[[package]]
name = "wcwidth"
version = "0.2.13"
description = "Measures the displayed width of unicode strings in a terminal"
optional = false
python-versions = "*"
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "f1694f707d895f0211021d90acef4c7449085c1c2cac19bbb35f14a7820dcaa6"

29
pyproject.toml Normal file
View File

@@ -0,0 +1,29 @@
[tool.poetry]
name = "sup"
version = "0.1.0"
description = ""
authors = ["Zev Averbach <zev@averba.ch>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
rich = "^13.7.0"
typer = "^0.9.0"
toml = "^0.10.2"
sqlmodel = "^0.0.14"
mysql-connector-python = "^8.3.0"
python-dotenv = "^1.0.1"
[tool.poetry.group.dev.dependencies]
pylint = "^3.0.3"
ruff = "^0.2.0"
black = "^24.1.1"
ipython = "^8.21.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
sup = "sup.cli:app"

110
scrape.js
View File

@@ -1,110 +0,0 @@
const getOrderElements = () => Array.from(document.querySelectorAll(".order-details-box"))
function getDateOfOrder(orderEl) {
console.log(orderEl);
return Date.parse(
orderEl
.children[0]
.children[1]
.innerText
.split("Delivered on ")
.slice(-1)[0]
);
}
const getSupps = orderEl => Array.from(orderEl.children[1].children[0].children);
SERVINGS_LOOKUP = {
"Liquid D-3 & MK-7": {
numUnitsInServing: .368,
servingUnit: "ml",
},
"Crucera-SGS": {
numUnitsInServing: 1,
servingUnit: "cap",
},
"Creatine": {
numUnitsInServing: 5,
servingUnit: "g",
},
"Liquid Iodine Plus": {
numUnitsInServing: .126,
servingUnit: "ml",
},
};
const getNumBottles = orderEl => parseInt(orderEl
.children[1]
.children[0]
.children[0]
.children[1]
.children[1]
.innerText
.split("Qty: ").slice(-1)[0]);
const SKIP_THESE = [
"Organic Brown Mustard",
"Sunflower Lecithin",
"Premium Whole Flaxseed",
"Organic Chia Seeds",
"Raw Macadamias",
"Hulled Hemp Seeds",
];
function makeSuppObj(sup) {
const ord = sup.parentElement.parentElement.parentElement;
const parts = sup.children[1].children[0].innerText.split(", ");
// TODO: if '(' in quantityText, use that as the serving
// in order to get the number of servings on hand
const quantityText = parts.slice(-1)[0];
const servingText = parts.slice(-2, -1)[0];
const name = parts[1];
if (SKIP_THESE.includes(name)) {
return null;
}
let numUnitsInServing, servingUnit;
if (!servingText.includes(" ")) {
const servingObj = SERVINGS_LOOKUP[name];
if (servingObj == undefined) {
throw {
name: "servingError",
level: "serious",
message: `couldn't get serving info for ${name} '(${parts})'`,
htmlMessage: this.message,
toString: function(){return this.name + ": " + this.message;}
};
} else {
console.log(servingObj);
({ numUnitsInServing, servingUnit } = servingObj);
}
} else {
[numUnitsInServing, servingUnit] = servingText.split(" ");
}
if (typeof numUnitsInServing !== "number") {
numUnitsInServing = numUnitsInServing.replace(",", "");
}
numUnitsInServing = parseInt(numUnitsInServing);
return {
orderDate: new Date(getDateOfOrder(ord)),
name,
quantity: parseInt(quantityText.split(" ")[0]),
servingUnit,
numUnitsInServing,
numBottles: getNumBottles(ord),
}
}
function main(orderCutoffHuman) {
const orderCutoff = Date.parse(orderCutoffHuman);
const orderEls = getOrderElements().filter(el => getDateOfOrder(el) > orderCutoff)
let suppEls = [];
for (const orderEl of orderEls) {
suppEls = suppEls.concat(getSupps(orderEl));
}
return suppEls.map(suppEl => makeSuppObj(suppEl)).filter(res => res != null);
}

179
scrape_iherb_orders.js Normal file
View File

@@ -0,0 +1,179 @@
let getOrderElements = () => Array.from(document.querySelectorAll(".order-details-box"))
function getDateOfOrder(orderEl) {
return Date.parse(
orderEl
.children[0]
.children[1]
.innerText
.split("Delivered on ")
.slice(-1)[0]
);
}
let getSupps = orderEl => Array.from(orderEl.children[1].children[0].children);
SERVINGS_LOOKUP = {
"Liquid D-3 & MK-7": {
numUnitsInServing: .368,
servingUnit: "ml",
},
"Crucera-SGS": {
numUnitsInServing: 1,
servingUnit: "caps",
},
"Creatine": {
numUnitsInServing: 5,
servingUnit: "g",
},
"Liquid Iodine Plus": {
numUnitsInServing: .126,
servingUnit: "ml",
},
"Magnesium Taurate": {
numUnitsInServing: 1,
servingUnit: "caps",
},
"High Absorption Magnesium Glycinate 350": {
numUnitsInServing: 350,
servingUnit: "mg",
},
"Iron Bisglycinate": {
numUnitsInServing: 1,
servingUnit: "caps",
},
"Extend-Release Magnesium": {
numUnitsInServing: 1,
servingUnit: "caps",
},
"Lutein & Zeaxanthin": {
numUnitsInServing: 1,
servingUnit: "caps",
},
"Aged Garlic Extract": {
numUnitsInServing: 600,
servingUnit: "mg",
},
"Organic Turmeric Curcumin": {
numUnitsInServing: 2250,
servingUnit: "mg",
},
"Lithium Orotate Drops": {
numUnitsInServing: .25,
servingUnit: "ml",
},
"Glycine": {
numUnitsInServing: 1000,
servingUnit: "mg",
},
"Super K": {
numUnitsInServing: 1500,
servingUnit: "mcg",
},
};
let getNumBottles = orderEl => parseInt(orderEl
.children[1]
.children[0]
.children[0]
.children[1]
.children[1]
.innerText
.split("Qty: ").slice(-1)[0]);
let SKIP_THESE = [
"Organic Brown Mustard",
"Sunflower Lecithin",
"Premium Whole Flaxseed",
"Organic Chia Seeds",
"Raw Macadamias",
"Hulled Hemp Seeds",
];
function makeSuppObj(sup) {
const ord = sup.parentElement.parentElement.parentElement;
const parts = sup.children[1].children[0].innerText.split(", ");
let quantityText = parts.slice(-1)[0];
let origQtyTxt = quantityText;
if (quantityText.includes("(")) {
quantityText = quantityText.match(/\((\d+)/).slice(-1)[0]
}
let quantityUnits = "caps";
if (!["caps", "capsules", "tablets", "softgels", "vegcaps", "lozenges"].some(str => origQtyTxt.toLowerCase().includes(str))) {
quantityUnits = origQtyTxt.match(/\(\d+ (.*)\)/).slice(-1)[0];
console.log(quantityUnits);
}
const servingText = parts.slice(-2, -1)[0];
const name = parts[1];
if (SKIP_THESE.includes(name)) {
return null;
}
let numUnitsInServing, servingUnit;
if (!servingText.includes(" ")) {
const servingObj = SERVINGS_LOOKUP[name];
if (servingObj == undefined) {
throw {
name: "servingError",
level: "serious",
message: `couldn't get serving info for ${name} '(${parts})'`,
htmlMessage: this.message,
toString: function(){return this.name + ": " + this.message;}
};
} else {
({ numUnitsInServing, servingUnit } = servingObj);
}
} else {
[numUnitsInServing, servingUnit] = servingText.split(" ");
}
if (typeof numUnitsInServing !== "number") {
numUnitsInServing = numUnitsInServing.replace(",", "");
}
let numUnitsInServingFinal = parseInt(numUnitsInServing);
if (!numUnitsInServingFinal || numUnitsInServingFinal == 0 || name.includes(servingUnit)) {
const servingObj = SERVINGS_LOOKUP[name];
if (!servingObj) {
throw {
name: "servingError",
level: "serious",
message: `couldn't get serving info for ${name} '(${parts})'`,
htmlMessage: this.message,
toString: function(){return this.name + ": " + this.message;}
};
}
({ numUnitsInServing: numUnitsInServingFinal, servingUnit } = servingObj);
}
return {
orderDate: new Date(getDateOfOrder(ord)),
name,
quantity: parseInt(quantityText.split(" ")[0]),
quantityUnits,
servingUnit,
numUnitsInServing: numUnitsInServingFinal,
numBottles: getNumBottles(ord),
}
}
function main(orderCutoffHuman, getFirst) {
let orderEls;
if (!getFirst) {
const orderCutoff = Date.parse(orderCutoffHuman);
orderEls = getOrderElements()
.filter(el => !el.innerText.includes("Cancelled"))
.filter(el => getDateOfOrder(el) > orderCutoff)
} else {
orderEls = [getOrderElements()[0]];
}
let suppEls = [];
for (const orderEl of orderEls) {
suppEls = suppEls.concat(getSupps(orderEl));
}
// TODO: write this to a file or copy to clipboard in CSV
return suppEls.map(suppEl => makeSuppObj(suppEl)).filter(res => res != null);
}
console.log(JSON.stringify(main(null, true)));

0
src/sup/__init__.py Normal file
View File

180
src/sup/_configs_to_sql.py Normal file
View File

@@ -0,0 +1,180 @@
import collections
from dataclasses import asdict
import datetime as dt
import hashlib
import json
import pathlib as pl
import tomllib
from mysql.connector.errors import IntegrityError
from sup.commands import (
add_user_supp_consumption,
add_inventory,
add_product,
add_alias,
set_fill_every_x_days,
set_last_fill_date,
create_user,
)
from sup.models import Supp
from sup.queries import product_is_in_products_table, alias_exists, inventory_exists, consumption_exists
from sup.sql_funcs import commit
def load_config():
return tomllib.loads(pl.Path('supps.toml').read_text())
CONFIG = load_config()
def transfer_inventory_to_sql():
inventory = json.loads(pl.Path('inventory.json').read_text())
for i in inventory:
add_inventory(
user_id='zev@averba.ch',
product_name=i['name'],
order_date=i['orderDate'][:10],
num_bottles=i['numBottles']
)
def populate_products():
inventory = json.loads(pl.Path('inventory.json').read_text())
for i in inventory:
try:
add_product(
name=i['name'],
quantity=i['quantity'],
num_units_in_serving=i['numUnitsInServing'],
quantity_units=i['quantityUnits'],
serving_units=i['servingUnit'],
)
except KeyError:
print(i)
raise
except IntegrityError:
print('already exists')
continue
commit()
def transfer_supps_consumption_toml_to_sql() -> None:
aliases = {v: k for k, v in CONFIG['product_aliases'].items()}
product_names = [i['name'] for i in json.loads(pl.Path('inventory.json').read_text())]
for sup in CONFIG["supps"]:
si = Supp(**sup)
si_dict = asdict(si)
_name = si_dict['name']
del si_dict['name']
name = None
if _name.lower() in aliases:
name = aliases[_name.lower()]
else:
for pn in product_names:
if _name.lower() in pn.lower():
name = pn
break
name = name or _name
print(name)
add_user_supp_consumption(name=name, user_id="zev@averba.ch", **si_dict)
commit()
def transfer_config_to_sql():
"""fill every x days and last fill"""
fill_every_x_days = CONFIG['FILL_EVERY_X_DAYS']
last_fill_date = CONFIG['LAST_FILL_DATE']
set_fill_every_x_days(value=fill_every_x_days, user_id="zev@averba.ch")
set_last_fill_date(value=last_fill_date, user_id="zev@averba.ch")
commit()
def transfer_aliases_to_sql():
aliases = CONFIG['product_aliases']
for name, alias in aliases.items():
add_alias("zev@averba.ch", name, alias)
commit()
def validate_products_against_toml():
inventory = json.loads(pl.Path('inventory.json').read_text())
for i in inventory:
if not product_is_in_products_table(name=i['name'], q=round(i['quantity']), num_units=i['numUnitsInServing']):
raise Exception
def do_create_user():
email = "zev@averba.ch"
pw = "@m_6Lwx.CjqvfwG@hmT"
first_name = "Zev"
last_name = "Averbach"
pw_hash = hashlib.new("SHA256")
pw_hash.update(pw.encode())
create_user(email, pw_hash.hexdigest(), first_name, last_name, dt.date.today(), 30)
commit()
print("okay, created user")
def validate_product_aliases():
aliases = CONFIG['product_aliases']
for name, alias in aliases.items():
if not alias_exists(name=name, alias=alias, user_id="zev@averba.ch"):
raise Exception
def validate_inventory():
inventory = json.loads(pl.Path('inventory.json').read_text())
for i in inventory:
if not inventory_exists(
user_id="zev@averba.ch",
product_name=i['name'],
order_date=i['orderDate'][:10],
quantity=i['quantity'],
serving_q=i["numUnitsInServing"],
):
raise Exception
# PRIMARY KEY (user_id, product_id, order_date)
def validate_consumption():
"""
Something is wrong here; several of the items in supps.toml are missing from
the consumption table.
"""
aliases = collections.defaultdict(list)
for k, v in CONFIG['product_aliases'].items():
aliases[v].append(k)
product_names = [i['name'] for i in json.loads(pl.Path('inventory.json').read_text())]
for sup in CONFIG["supps"]:
si = Supp(**sup)
si_dict = asdict(si)
_name = si_dict['name']
del si_dict['name']
name = None
names = None
if _name in aliases:
names = aliases[_name]
print(f"{names=}")
else:
for pn in product_names:
if _name.lower() in pn.lower():
name = pn
break
if names:
if not any(consumption_exists(name=n, user_id="zev@averba.ch", **si_dict) for n in names):
raise Exception
else:
name = name or _name
if not consumption_exists(name=name, user_id="zev@averba.ch", **si_dict):
raise Exception
# do_create_user()
# populate_products()
# transfer_inventory_to_sql()
# transfer_aliases_to_sql()
# transfer_supps_consumption_toml_to_sql()
# transfer_config_to_sql()
# validate_products_against_toml()
# validate_product_aliases()
# validate_inventory()
# validate_consumption()

164
src/sup/_create_tables.py Normal file
View File

@@ -0,0 +1,164 @@
import atexit
import os
import mysql.connector
from mysql.connector import errorcode
from dotenv import load_dotenv
load_dotenv()
DB_NAME = os.getenv("PLANETSCALE_DATABASE")
cnx = mysql.connector.connect(
user=os.getenv("PLANETSCALE_USERNAME"),
password=os.getenv("PLANETSCALE_PW"),
host=os.getenv("PLANETSCALE_HOST"),
database=DB_NAME,
)
cursor = cnx.cursor()
def close_everything():
cnx.close()
cursor.close()
atexit.register(close_everything)
def do_query(create_statement: str) -> None:
table_name = None
try:
table_name = create_statement.lower().split("create table ")[1].split(" ")[0]
except IndexError:
try:
table_name = create_statement.lower().split("drop table ")[1].split(" ")[0]
except IndexError:
pass
print(f"{table_name=}")
print(create_statement)
try:
cursor.execute(create_statement)
except mysql.connector.Error as err:
if err.errno == errorcode.ER_TABLE_EXISTS_ERROR:
print("already exists.")
else:
print(err.msg)
return
print(f"created table '{table_name}'")
def create_table_users():
query = ("""
create table users (
id VARCHAR(80) NOT NULL,
pw_hash VARCHAR(64) NOT NULL,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
fill_every_x_days INTEGER NOT NULL,
last_fill_date DATE NOT NULL,
CONSTRAINT pk_user PRIMARY KEY (id)
)""").strip()
do_query(query)
def create_table_products():
query = ("""
create table products (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(150) NOT NULL,
quantity INT NOT NULL,
quantity_units ENUM('caps', 'mg', 'g', 'ml', 'mcg', 'mcl', 'iu'),
serving_units ENUM('caps', 'mg', 'g', 'ml', 'mcg', 'mcl', 'iu'),
num_units_in_serving INT NOT NULL,
CONSTRAINT pk_product_name PRIMARY KEY (id, name),
CONSTRAINT uq_name_quantity_numunitsinserving UNIQUE KEY (name, quantity, num_units_in_serving)
)""").strip()
do_query(query)
def create_table_product_aliases():
query = ("""
CREATE TABLE user_product_aliases (
user_id VARCHAR(80) NOT NULL,
product_id INT NOT NULL,
alias VARCHAR(30) NOT NULL,
CONSTRAINT fk_product_alias_user_id FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_product_alias_product_id FOREIGN KEY (product_id) REFERENCES products(id),
CONSTRAINT pk_user_product_alias PRIMARY KEY (user_id, product_id, alias)
)""").strip()
do_query(query)
def create_table_user_supplements_consumption():
query = ("""
create table user_supplements_consumption (
user_id VARCHAR(80) NOT NULL,
product_id INT NOT NULL,
morning INT DEFAULT 0,
lunch INT DEFAULT 0,
dinner INT DEFAULT 0,
bedtime INT DEFAULT 0,
days_per_week INT DEFAULT 7,
units ENUM('caps', 'mg', 'g', 'ml', 'mcg', 'mcl', 'iu'),
winter_only BOOL DEFAULT false,
CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_product_id FOREIGN KEY (product_id) REFERENCES products(id),
CONSTRAINT pk_user_supplement_consumption PRIMARY KEY (user_id, product_id, morning)
)""").strip().replace("\n", "")
do_query(query)
def create_table_user_supplements_orders():
query = ("""
create table user_supplements_orders (
user_id VARCHAR(80) NOT NULL,
product_id INT NOT NULL,
order_date DATE NOT NULL,
num_bottles INT NOT NULL,
CONSTRAINT fk_user_supplements_orders_user_id FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_user_supplements_orders_product_id FOREIGN KEY (product_id) REFERENCES products(id),
CONSTRAINT pk_user_supplement_order PRIMARY KEY (user_id, product_id, order_date)
)""").strip()
do_query(query)
def create_tables1():
try:
create_table_users()
except Exception:
return
try:
create_table_products()
except Exception:
do_query("drop table users")
return
def create_tables2():
try:
create_table_user_supplements_consumption()
except Exception:
return
try:
create_table_user_supplements_orders()
except Exception:
do_query("drop table user_supplements_consumption")
try:
create_table_product_aliases()
except Exception:
do_query("drop table user_supplements_consumption")
do_query("drop table user_supplements_orders")
def drop_tables():
do_query("drop table user_supplements_consumption")
do_query("drop table user_supplements_orders")
do_query("drop table user_product_aliases")
do_query("drop table users")
do_query("drop table products")
if __name__ == "__main__":
drop_tables()
create_tables1()
create_tables2()

209
src/sup/cli.py Normal file
View File

@@ -0,0 +1,209 @@
import datetime as dt
import json
import math
import toml
from typing_extensions import Annotated
import typer
from sup.main import (
load_ordered_supps,
ORDERED_SUPPS_FP,
load_config,
SUPP_CONSUMPTION_FP,
load_inventory,
)
from sup.models import Supp
from sup.queries import get_user
app = typer.Typer()
class UnitMismatch(Exception):
pass
class Missing(Exception):
pass
def get_qty_inventory(supp: Supp, inventories: list[dict], next_fill_date: dt.date, last_fill_date: dt.date) -> int:
inv = 0
CHECKING = ("broccomax", "glycine", "ginger root")
for inventory in inventories:
inventory_order_date = dt.datetime.strptime(
inventory["orderDate"][:10], "%Y-%m-%d"
).date()
if inventory_order_date > last_fill_date:
num_days_since_bought = 0
else:
num_days_since_bought = (next_fill_date - inventory_order_date).days
if supp.name in CHECKING:
print()
print(supp.name)
print(f"{inventory_order_date=}")
print(f"{num_days_since_bought=}")
if inventory["servingUnit"] != supp.units:
raise UnitMismatch(inventory, supp)
supply_units = inventory["quantity"] * inventory["numBottles"] * inventory["numUnitsInServing"]
if supp.name in CHECKING:
print(f"{supply_units=}")
inv += supply_units - (supp * num_days_since_bought)
if inv < 0 and (
last_fill_date > inventory_order_date
or (next_fill_date - last_fill_date).days > 9 * 30
):
inv = 0
if supp.name in CHECKING:
print(f"{inv=} for {supp.name=}")
print()
return inv
def get_num_winter_days_starting(num_days: int, starting: dt.date) -> int:
# dec 21 to mar 20
winter_starts = dt.date(starting.year, 12, 21)
if starting <= winter_starts:
num_days_til_winter = (winter_starts - starting).days
if num_days_til_winter > num_days:
return 0
return num_days - num_days_til_winter
winter_ends = dt.date(starting.year, 3, 2)
if starting >= winter_ends:
winter_starts = dt.date(starting.year + 1, 12, 21)
num_days_til_winter = (winter_starts - starting).days
if num_days_til_winter > num_days:
return 0
return num_days - num_days_til_winter
winter_ends = dt.date(starting.year + 1, 3, 2)
if starting <= winter_ends:
num_days_til_winter_ends = (winter_ends - starting).days
if num_days_til_winter_ends > num_days:
return num_days
return num_days_til_winter_ends
raise Exception
@app.command()
def status():
"""
check if there's enough inventory for the next fill-up; if not, what to order?
TODO: this doesn't seem to sense pending orders which have been added to inventory.json
maybe because the delivery date is in the present/future?
"""
# TODO: make this an arg
user_id = "zev@averba.ch"
user = get_user(user_id)
num_days_of_inventory_needed = user["fill_every_x_days"]
last_fill_date = dt.datetime.strptime(
user["last_fill_date"], "%Y-%m-%d"
).date()
next_fill_date = last_fill_date + dt.timedelta(num_days_of_inventory_needed)
print()
print(f"{next_fill_date=}")
print()
# TODO: this is where you left off
inventory = load_inventory(user_id)
needs = []
num_days_of_inventory_needed_winter = get_num_winter_days_starting(
num_days_of_inventory_needed, next_fill_date
)
for sup in config["supps"]:
sup_inst = Supp(**sup)
if sup_inst.winter_only:
qty_needed = sup_inst * num_days_of_inventory_needed_winter
else:
qty_needed = sup_inst * num_days_of_inventory_needed
try:
invs = inventory[sup_inst.name.lower()]
except KeyError:
print(f"no hit for key '{sup_inst.name}'")
qty_of_inventory = 0
needs.append((sup_inst.name, 0, float("inf")))
continue
qty_of_inventory = get_qty_inventory(sup_inst, invs, next_fill_date, last_fill_date)
net_need = int(qty_needed - qty_of_inventory)
inv = invs[-1]
if net_need > 0:
num_units_needed = net_need / inv["numUnitsInServing"] # type: ignore
num_bottles_needed = int(math.ceil(num_units_needed / inv["quantity"])) # type: ignore
needs.append((sup_inst.name, int(num_units_needed), num_bottles_needed))
if needs:
print()
print(f"The next fill-up is on {next_fill_date}, and you won't have enough of:")
print()
for name, units_needed, num_bottles in needs:
bottle = "bottle" if num_bottles == 1 else "bottles"
print(
f"{name} (need {units_needed} units, which is {num_bottles} {bottle})"
)
print()
@app.command()
def fill():
"""reset 'next fill' clock to today"""
validate_matches()
config = load_config()
today = dt.date.today()
config["LAST_FILL_DATE"] = today.strftime("%Y-%m-%d")
fill_every_x_days = config["FILL_EVERY_X_DAYS"]
save_config(config)
print(
f"Okay, next fill-up set to {today + dt.timedelta(days=fill_every_x_days)} (configured in `sup.toml`::FILL_EVERY_X_DAYS)"
)
@app.command()
def add(
name: Annotated[str, typer.Option(prompt=True)],
quantity: Annotated[int, typer.Option(prompt=True)],
serving_quantity: Annotated[int, typer.Option(prompt=True)],
serving_unit: Annotated[str, typer.Option(prompt=True)] = "mg",
quantity_unit: Annotated[str, typer.Option(prompt=True)] = "caps",
date: (
Annotated[dt.datetime, typer.Option(help="(today)", prompt=True)] | None
) = None,
number_of_bottles: Annotated[int, typer.Option(prompt=True)] = 1,
) -> None:
"""add to inventory"""
if date is None:
date = dt.datetime.now().date() # type: ignore
else:
date = date.date() # type: ignore
ordered_supps = load_ordered_supps(user_id)
order_dict = dict(
name=name,
quantity=quantity,
numUnitsInServing=serving_quantity,
servingUnit=serving_unit,
quantityUnit=quantity_unit,
orderDate=date.strftime("%Y-%m-%d"), # type: ignore
numBottles=number_of_bottles,
)
ordered_supps.append(order_dict)
save_ordered_supps(ordered_supps)
print(f"added {order_dict} to {ORDERED_SUPPS_FP}")
def save_ordered_supps(ordered_supps: list[dict]) -> None:
ORDERED_SUPPS_FP.write_text(json.dumps(ordered_supps, indent=2))
def save_config(config: dict) -> None:
SUPP_CONSUMPTION_FP.write_text(toml.dumps(config))

60
src/sup/commands.py Normal file
View File

@@ -0,0 +1,60 @@
import datetime as dt
import typing as t
from sup.sql_funcs import do_query
def set_fill_every_x_days(value: int, user_id: str) -> None:
do_query(query=f"update users set fill_every_x_days = {value} where id = '{user_id}'")
def set_last_fill_date(value: int, user_id: str) -> None:
do_query(query=f"update users set last_fill_date = '{value}' where id = '{user_id}'")
def create_user(email: str, pw_hash: str, first_name: str, last_name: str, last_fill_date: dt.date, fill_every_x_days=30):
do_query(commit=True,
query="insert into users (id, pw_hash, first_name, last_name, last_fill_date, fill_every_x_days) values "
f"('{email}', '{pw_hash}', '{first_name}', '{last_name}', '{last_fill_date}', '{fill_every_x_days}')"
)
def add_user_supp_consumption(user_id: str, name: str, morning: int, lunch: int, dinner: int, bedtime: int, days_per_week: int, units: str, winter_only: bool):
do_query(
"insert into user_supplements_consumption "
"(user_id, product_id, morning, lunch, dinner, bedtime, days_per_week, units, winter_only) "
f"select '{user_id}', id, {morning}, {lunch}, {dinner}, {bedtime}, {days_per_week}, '{units}', {winter_only} "
f"from products where name = '{name}'"
)
def add_inventory(user_id: str, product_name: str, order_date: str, num_bottles: int) -> None:
do_query(
"insert into user_supplements_orders (user_id, product_id, order_date, num_bottles) "
f"select '{user_id}', id, '{order_date}', {num_bottles} "
f"from products where name = '{product_name}'"
)
def add_product(
name: str,
quantity: int,
quantity_units: t.Literal['caps', 'mg', 'g', 'ml', 'mcg', 'iu'],
serving_units: t.Literal['caps', 'mg', 'g', 'ml', 'mcg', 'iu'],
num_units_in_serving: int,
):
query = (
"insert into products (name, quantity, quantity_units, serving_units, num_units_in_serving) values ("
f"'{name}', {quantity}, '{quantity_units}', '{serving_units}', {num_units_in_serving}"
")"
)
do_query(query)
def add_alias(user_id: str, name: str, alias: str) -> None:
query = (
"insert into user_product_aliases (user_id, product_id, alias) "
f"select '{user_id}', id, '{alias}' "
f"from products where name = '{name}'"
)
do_query(query)

43
src/sup/main.py Normal file
View File

@@ -0,0 +1,43 @@
"""
TODO:
special case: Liquid D-3 & MK-7 -- has d-3 and k-mk7
special case: K Complex has K1, MK-4 and MK-7 in it
"""
import collections
import json
import pathlib as pl
import tomllib
ORDERED_SUPPS_FP = pl.Path("inventory.json")
SUPP_CONSUMPTION_FP = pl.Path("supps.toml")
def load_config():
return tomllib.loads(SUPP_CONSUMPTION_FP.read_text())
def load_ordered_supps(user_id: str) -> list[dict]:
return json.loads(ORDERED_SUPPS_FP.read_text())
CONFIG = load_config()
ALIASES = CONFIG["product_aliases"]
ordered_supps = load_ordered_supps(user_id)
for i in CONFIG["supps"]:
for ordered_supp in ordered_supps:
if i["name"].lower() in ordered_supp["name"].lower():
ALIASES[ordered_supp["name"]] = i["name"]
ALIASES_REV = {v: k for k, v in ALIASES.items()}
def load_inventory(user_id: str):
inventory = collections.defaultdict(list)
for s in load_ordered_supps(user_id):
if s["name"] not in CONFIG["discontinued"]:
inventory[ALIASES[s["name"]]].append(s)
return inventory

26
src/sup/models.py Normal file
View File

@@ -0,0 +1,26 @@
from dataclasses import dataclass
import typing as t
@dataclass
class Supp:
name: str
morning: int | float = 0
lunch: int | float = 0
dinner: int | float = 0
bedtime: int | float = 0
days_per_week: int = 7
units: t.Literal["caps", "mg", "g", "ml", "mcg", "mcl", "iu"] = "mg"
winter_only: bool = False
def __mul__(self, other: int) -> float:
return self.quantity_per_day * other
@property
def quantity_per_day(self) -> float:
return (
sum([self.morning, self.lunch, self.dinner, self.bedtime])
* self.days_per_week
/ 7
)

59
src/sup/queries.py Normal file
View File

@@ -0,0 +1,59 @@
from sup.sql_funcs import do_query
def get_user(user_id: str):
return do_query(f"select * from users where id = '{user_id}'", get_result=True, return_dict=True)[0] # type: ignore
def product_is_in_products_table(name: str, q: int, num_units: int) -> bool:
query = f"select count(*) from products where name = '{name}' and quantity = {q} and num_units_in_serving = {num_units}"
res = do_query(get_result=True, query=query)
if res[0][0] != 1: # type: ignore
print(res[0][0]) # type: ignore
return False
return True
def alias_exists(name: str, alias: str, user_id: str) -> bool:
query = (
f"select count(*) from user_product_aliases where user_id = '{user_id}' "
f"and alias = '{alias}' and product_id = (select id from products where name = '{name}')"
)
res = do_query(get_result=True, query=query)
if res[0][0] != 1: # type: ignore
print(res[0][0]) # type: ignore
return False
return True
def inventory_exists(user_id: str, product_name: str, order_date: str, quantity: int, serving_q: int):
query = (
f"select count(*) from user_supplements_orders where user_id = '{user_id}' and order_date = '{order_date}' "
"and product_id = ("
f"select distinct id from products where name = '{product_name}' and quantity = {quantity} "
f"and num_units_in_serving = {serving_q}"
")"
)
res = do_query(get_result=True, query=query)
if res[0][0] != 1: # type: ignore
print(res[0][0]) # type: ignore
return False
return True
def consumption_exists(name: str, user_id: str, morning: int, lunch: int, dinner: int, bedtime: int, days_per_week: int, units: str, winter_only: bool):
name = name.lower()
query = (
f"select count(*) from user_supplements_consumption where user_id = '{user_id}' and morning = {morning} "
f"and lunch = {lunch} and dinner = {dinner} and bedtime = {bedtime} and days_per_week = {days_per_week} "
f"and units = '{units}' and winter_only = {winter_only} "
f"and product_id = coalesce("
f"(select id from products where name = '{name}' limit 1), "
f"(select product_id from user_product_aliases where alias = '{name}' and user_id = '{user_id}' limit 1)"
")"
)
res = do_query(get_result=True, query=query)
if res[0][0] != 1: # type: ignore
print(res[0][0]) # type: ignore
return False
return True

42
src/sup/sql_funcs.py Normal file
View File

@@ -0,0 +1,42 @@
import atexit
import os
from dotenv import load_dotenv
import mysql.connector
load_dotenv()
DB_NAME = os.getenv("PLANETSCALE_DATABASE")
cnx = mysql.connector.connect(
user=os.getenv("PLANETSCALE_USERNAME"),
password=os.getenv("PLANETSCALE_PW"),
host=os.getenv("PLANETSCALE_HOST"),
database=DB_NAME,
)
cursor = cnx.cursor(buffered=True)
dict_cursor = cnx.cursor(dictionary=True)
def close_everything():
cnx.close()
cursor.close()
atexit.register(close_everything)
def do_query(query: str, get_result: bool = False, commit: bool = False, return_dict: bool = False) -> list | None:
print(query)
c = cursor if not return_dict else dict_cursor
try:
c.execute(query)
except mysql.connector.Error as err:
print(err.msg)
raise
if commit:
cnx.commit()
if get_result:
return cursor.fetchall()
def commit():
cnx.commit()

216
supps.toml Normal file
View File

@@ -0,0 +1,216 @@
discontinued = [ "Magnesium Taurate +", "NAD+ Cell Regenerator", "Fisetin with Novusetin", "Fisetin Novusetin", "Collagen",]
FILL_EVERY_X_DAYS = 30
LAST_FILL_DATE = "2024-01-16"
[[supps]]
name = "ashwagandha"
bedtime = 600
[[supps]]
name = "aspirin"
lunch = 86
days_per_week = 3
[[supps]]
name = "astaxanthin"
morning = 12
[[supps]]
name = "b complex"
morning = 1
days_per_week = 2
units = "caps"
[[supps]]
name = "b-12"
lunch = 1000
units = "mcg"
days_per_week = 1
[[supps]]
name = "boron"
morning = 2
[[supps]]
name = "broccomax"
morning = 1
dinner = 1
units = "caps"
[[supps]]
name = "vitamin c"
morning = 500
[[supps]]
name = "creatine"
lunch = 2500
days_per_week = 4
[[supps]]
name = "Ca-AKG"
morning = 1000
dinner = 1000
[[supps]]
name = "coq10"
morning = 100
lunch = 100
dinner = 100
[[supps]]
name = "d-3"
morning = 5000
units = "iu"
winter_only = true
[[supps]]
name = "dhea"
morning = 25
[[supps]]
name = "vitamin e"
morning = 67
days_per_week = 3
[[supps]]
name = "epa/dha"
morning = 1280
dinner = 640
[[supps]]
name = "garlic"
morning = 1200
dinner = 1200
[[supps]]
name = "genistein"
morning = 125
[[supps]]
name = "ginger root"
morning = 2200
dinner = 2200
[[supps]]
name = "glucosamine sulfate"
morning = 1500
dinner = 1500
[[supps]]
name = "glycine"
morning = 2000
[[supps]]
name = "hyaluronic acid"
morning = 300
[[supps]]
name = "iodine"
morning = 126
units = "mcl"
[[supps]]
name = "iron"
morning = 10
[[supps]]
name = "k1"
morning = 1500
units = "mcg"
[[supps]]
name = "k2-mk7"
morning = 300
units = "mcg"
[[supps]]
name = "k2-mk4"
morning = 5
[[supps]]
name = "l-lysine"
morning = 1000
dinner = 1000
[[supps]]
name = "l-tyrosine"
dinner = 500
[[supps]]
name = "lithium orotate"
morning = 1000
units = "mcg"
[[supps]]
name = "lycopene"
morning = 10
[[supps]]
name = "magnesium slow release"
morning = 1
units = "caps"
[[supps]]
name = "magnesium taurate"
bedtime = 125
[[supps]]
name = "magnesium glycinate"
bedtime = 350
[[supps]]
name = "magtein"
bedtime = 2000
[[supps]]
name = "methyl folate"
morning = 1700
units = "mcg"
[[supps]]
name = "nac"
morning = 1800
dinner = 1800
[[supps]]
name = "pqq"
morning = 20
[[supps]]
name = "probiotic"
morning = 1
units = "caps"
[[supps]]
name = "taurine"
morning = 2000
dinner = 1000
[[supps]]
name = "turmeric"
morning = 1000
dinner = 1000
[[supps]]
name = "zeaxanthin"
morning = 20
days_per_week = 3
[[supps]]
name = "zinc"
morning = 15
[product_aliases]
"Naturally Sourced Vitamin E" = "vitamin e"
"Natural Vitamin K2 MK-7 with MenaQ7" = "k2-mk7"
"Liquid D-3 & MK-7" = "d-3"
"Calcium AKG Longevity" = "ca-akg"
"MK-7 Vitamin K-2" = "k2-mk7"
"Optimized Folate" = "methyl folate"
"Extend-Release Magnesium" = "magnesium slow release"
Crucera-SGS = "broccomax"
B-50 = "b complex"
"Vitamin K2, MK-4 (Menatetrenone)" = "k2-mk4"
"Super K" = "k1"
"Ultimate Omega, Lemon" = "epa/dha"
"Ultimate Omega" = "epa/dha"
Lithium = "lithium orotate"