Compare commits

...

2 commits

Author SHA1 Message Date
Funky Waddle 05aad26e21 Fix merge conflicts 2025-12-18 09:54:27 -06:00
Funky Waddle 6779aa3b80 Initial commit 2025-12-18 09:49:31 -06:00
5 changed files with 594 additions and 1 deletions

2
.gitignore vendored
View file

@ -158,7 +158,7 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
# ---> JetBrains
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider

123
MILESTONES.md Normal file
View file

@ -0,0 +1,123 @@
### FunkyJuice Recipes — Project Milestones
This document outlines the sequence of milestones to deliver the FunkyJuice Recipes desktop app using Toga, Peewee (SQLite), and Briefcase. Each milestone is scoped to be achievable in a short iteration and includes clear acceptance criteria.
---
### Milestone 1 — Project skeleton and tooling
- Set up `src/funkyjuicerecipes/` package layout per README.
- Add minimal `app.py` with `FunkyJuiceRecipesApp` and `main()` returning the app.
- Configure `pyproject.toml` (already added) and ensure `briefcase dev` can start a blank window.
- Add `tests/` folder and test runner config.
Acceptance:
- `briefcase dev` launches an empty Toga window titled “FunkyJuice Recipes”.
- `pytest` runs (even with zero or smoke tests).
---
### Milestone 2 — Database initialization and migrations framework
- Implement `data/db.py` using `app.paths.data` and Peewee pragmas (`foreign_keys=1`, WAL).
- Create migrations table and a simple migration runner that detects unapplied migration files by filename.
- Implement initial migrations for `flavor`, `recipe`, and `recipe_flavor` tables matching README DDL.
Acceptance:
- On first launch, DB file is created in the peruser data directory, tables exist, and migrations table is populated.
- Subsequent launches do not re-run applied migrations.
---
### Milestone 3 — Peewee models and DAOs
- Implement `Flavor`, `Recipe`, `RecipeFlavor` models reflecting constraints (UNIQUEs, FKs, CHECKs where applicable in logic).
- Provide DAO/helper functions for common ops (create/list/get/update/delete) with soft-delete semantics for flavors.
- Enable case-insensitive uniqueness for `Flavor(name, company)`.
Acceptance:
- Unit tests can create flavors (no duplicates ignoring case), recipes, and link ingredients; soft-deleted flavors are excluded by default queries.
---
### Milestone 4 — Calculation engine
- Implement pure functions in `logic/calculations.py` to compute PG/VG/Nic and per-flavor mL from inputs (percents + size).
- Include validation (non-negative PG/VG; base_pg + base_vg = 100; percent totals ≈ 100 within tolerance).
- Define rounding/display policy (e.g., 1 decimal for View screen).
Acceptance:
- Tests cover the README example (120 mL case) and edge cases (nic in PG vs VG, excessive flavor %).
---
### Milestone 5 — Main window and navigation shell
- Build `MainWindow`: menu bar (File/New Recipe/Exit; Help/About), buttons for Inventory, recipe list placeholder.
- Wire basic screens/routes (View Recipe, Add/Edit Recipe, Inventory) with placeholders.
Acceptance:
- App displays main list and can navigate to empty placeholder screens and back.
---
### Milestone 6 — Inventory screen (CRUD + soft delete)
- Implement `FlavorsTable` with add/edit/delete.
- Enforce no duplicates; on delete, soft-delete if referenced by recipes; otherwise hard-delete.
- Provide toggle to view soft-deleted flavors and actions to undelete or hard-delete when safe.
Acceptance:
- Users can manage flavors per README, with correct soft/hard delete behavior.
---
### Milestone 7 — Add/Edit Recipe screens
- Implement `RecipeForm` with PG/VG base, Nic % and base, size, and flavor lines (select existing flavors + percentage).
- Auto-maintain PG + VG = 100; prevent negatives.
- On save: validate, combine duplicate flavors (prompt as specified), persist recipe + ingredients.
Acceptance:
- Users can create and edit recipes meeting validation rules and duplicate-combine flow.
---
### Milestone 8 — View Recipe screen
- Render computed components (PG, VG, Nic, flavors) using calculation engine, rounded to 1 decimal.
- Support view mode toggle: Individual Flavors vs Flavor Prep total.
- Provide action to open Edit, Delete confirmation, and Inventory.
Acceptance:
- A saved recipe displays correct amounts; toggle updates view accordingly.
---
### Milestone 9 — Flavor Prep mixer utility
- Implement “Mix Flavor Prep” dialog/workflow computing batch amounts from recipe flavor ratios for selectable sizes (10/30/120 mL, etc.).
- No persistence for Flavor Prep (ephemeral), but support printing/exporting batch breakdown (optional CSV).
Acceptance:
- Users can choose a batch size and see per-flavor mL for the prep; closing returns to View Recipe.
---
### Milestone 10 — Polishing, About, and edge behaviors
- Implement About dialog (app name, version, author).
- Confirm unsaved-changes prompts on screen change and app exit while editing.
- Empty states, error dialogs, and friendly validation messages.
Acceptance:
- UX polish complete; no obvious dead-ends; dialogs work across platforms.
---
### Milestone 11 — Packaging and distribution
- Verify `briefcase dev` on target OS; add icons under `resources/`.
- Build installers with Briefcase for target platforms.
- Document install/run instructions and DB location.
Acceptance:
- Installers build successfully; smoke test of installed app passes.
---
### Milestone 12 — Testing and CI (optional but recommended)
- Add unit tests for models/DAOs and calculations; UI smoke tests using Toga Dummy backend where feasible.
- Set up a simple CI (e.g., GitHub Actions) to run tests on push.
Acceptance:
- CI passes on main branch; coverage of calculation and DAO logic acceptable.

332
SPECS.md Normal file
View file

@ -0,0 +1,332 @@
# FunkyJuice Recipes
A python application for managing e-juice recipes
* Uses SQLite3 as database
* Uses Toga as GUI
* Uses Peewee ORM
* Separate files per model
* Separate files per migration
* Migrations for database schema changes using `playhouse.migrate`
* Migrations are versioned via the date they were created (YYYY-MM-DD_migration_name.py)
* Migrations state is stored in a Migrations table in the database
* Migrations table does not use a migration file of its own
* Migrations are run automatically on startup (check migration files vs database entries in migrations table)
* At startup
* The app creates (if missing) the peruser data directory at `app.paths.data`
* The app stores the SQLite DB at `<app.paths.data>/funkyjuicerecipes.data`.
* The connection enables pragmas: `{'foreign_keys': 1, 'journal_mode': 'wal', 'synchronous': 1}`.
* Tests use a temporary DB path.
* Uses class wrappers for Toga widgets
* FunkyJuiceRecipesApp(toga.App)
* MainWindow(toga.MainWindow)
* RecipesTable(toga.Table)
* RecipeForm(toga.Box -> with form elements as needed)
* AddRecipeButton(toga.Button)
* EditRecipeButton(toga.Button)
* DeleteRecipeButton(toga.Button -> with confirmation dialog)
* FlavorsTable(toga.Table)
* AddFlavorButton(toga.Button)
* EditFlavorButton(toga.Button)
* DeleteFlavorButton(toga.Button -> with confirmation dialog)
* FlavorForm(toga.Box -> with form elements as needed)
* Uses briefcase to package app for distribution
* Code follows PEP8
* Code follows SOLID principles
* SRP at method level
## How to Run
* `pip install briefcase`
* or `uv add briefcase`
* `briefcase dev`
* or `python -m funkyjuicerecipes.app`
## Flavors Soft Delete Policy
* When a flavor is deleted
* Flavors are Soft Deleted if they are in a recipe.
* Flavors are Hard Deleted if they are not in a recipe.
* Flavors can be Un-Deleted when viewing the Soft Deleted Flavors list.
* Flavors can be Hard Deleted when viewing the Soft Deleted List and they are no longer in a recipe.
* UI Queries default to `WHERE is_deleted = 0`
* Hard Delete is only allowed when `NOT EXISTS (SELECT 1 FROM recipe_flavor WHERE flavor_id=flavor.id)`
## Directory/File Structure
* project-root/
* pyproject.toml
* README.md
* src/
* funkyjuicerecipes/
* __init__.py
* app.py # Toga App subclass and `main()`
* ui/
* __init__.py
* buttons/
* __init__.py
* add_recipe_button.py
* edit_recipe_button.py
* delete_recipe_button.py
* add_flavor_button.py
* edit_flavor_button.py
* delete_flavor_button.py
* forms/
* __init__.py
* recipe_form.py
* flavor_form.py
* tables/
* __init__.py
* recipes_table.py
* flavors_table.py
* dialogs/
* __init__.py
* delete_recipe_confirmation.py
* delete_flavor_confirmation.py
* data/
* __init__.py
* db.py # database init (Peewee SqliteDatabase, pragmas)
* models/
* __init__.py
* flavor.py
* recipe.py
* recipe_flavor.py
* migrations/
* __init__.py
* 2025-12-18_flavor_table_created.py
* 2025-12-18_recipe_table_created.py
* 2025-12-18_recipe_flavor_table_created.py
* logic/
* __init__.py
* calculations.py # pure functions for PG/VG/nic/flavor math
* resources/ # icons, etc.
* tests/
* test_calculations.py
## Requirements
* Python 3.8+
* Toga
* Peewee
* playhouse.migrate
* Briefcase
## DB Schema
* Enable FKs on connection: `SqliteDatabase(path, pragmas={'foreign_keys': 1})`
* Flavor Table
```
flavor(
id INTEGER PRIMARY KEY,
name TEXT COLLATE NOCASE NOT NULL,
company TEXT COLLATE NOCASE NOT NULL,
base TEXT CHECK(base IN ('PG','VG')) NOT NULL,
is_deleted INTEGER NOT NULL DEFAULT 0,
deleted_at DATETIME NULL,
UNIQUE(name, company)
)
CREATE INDEX IF NOT EXISTS idx_flavor_name_company ON flavor(name, company);
```
* Recipe Table
```
recipe(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
size_ml INTEGER NOT NULL,
base_pg_pct REAL NOT NULL,
base_vg_pct REAL NOT NULL,
nic_pct REAL CHECK(nic_pct >= 0 AND nic_pct <= 100) NOT NULL,
nic_base TEXT CHECK(nic_base IN ('PG','VG')) NOT NULL,
CHECK (
base_pg_pct >= 0 AND
base_vg_pct >= 0 AND
base_pg_pct + base_vg_pct = 100
)
)
```
* RecipeFlavor XREF Table
```
recipe_flavor(
id INTEGER PRIMARY KEY,
recipe_id INTEGER NOT NULL,
flavor_id INTEGER NOT NULL,
pct REAL CHECK(pct >= 0 AND pct <= 100) NOT NULL,
FOREIGN KEY(recipe_id)
REFERENCES recipe(id) ON DELETE CASCADE,
FOREIGN KEY(flavor_id) REFERENCES flavor(id) ON DELETE RESTRICT,
UNIQUE(recipe_id, flavor_id)
)
CREATE INDEX IF NOT EXISTS idx_recipe_flavor_recipe ON recipe_flavor(recipe_id);
CREATE INDEX IF NOT EXISTS idx_recipe_flavor_flavor ON recipe_flavor(flavor_id);
```
* Migrations Table
```
migrations(
id INTEGER PRIMARY KEY,
batch INTEGER NOT NULL,
filename TEXT NOT NULL,
date_run DATETIME NOT NULL,
UNIQUE(filename)
)
```
## Recipes
* Recipes require
* Name,
* Size (ml),
* PG Base %,
* VG base %,
* Nic Base (PG/VG)
* Nic %,
* Flavor List
* Recipe equations are:
* flavor_pg_pct = sum(pct for flavor in flavors if flavor.base == 'PG')
* flavor_vg_pct = sum(pct for flavor in flavors if flavor.base == 'VG')
* pg_pct = base_pg_pct - flavor_pg_pct - (nic_pct if nic_base == 'PG' else 0)
* vg_pct = base_vg_pct - flavor_vg_pct - (nic_pct if nic_base == 'VG' else 0)
* PG_ml = pg_pct / 100 * size_ml
* VG_ml = vg_pct / 100 * size_ml
* Nic_ml = nic_pct / 100 * size_ml
* Flavor_i_ml = flavor_i.pct / 100 * size_ml
* Recipe output is:
* Recipe Name
* Size (ml)
* PG: PG_ml
* VG: VG_ml
* Nic: Nic_ml
* Flavors
* If viewing individual flavors:
* Flavor 1 Name: Flavor_1_ml
* Flavor 2 Name: Flavor_2_ml
* if viewing Flavor Prep:
* "Flavor Prep": Flavor_Prep_ml (sum(pct for flavor in flavors)/100 * size_ml)
## Main Screen
* Menu Bar with File, Recipes, and Help
* File Menu
* New Recipe
* Exit
* Recipes Menu
* Shows a list of saved recipes as menu items
* Selecting menu item opens the recipe screen
* Help Menu
* About screen
* Shows Saved Recipes List
* Recipe Name
* Button to open the recipe screen (View only)
* Button to edit the recipe (Edit form)
* Button to delete the recipe (Confirmation dialog)
* Has button on top to open Inventory Screen.
## About Screen
* Shows
* Application Name (FunkyJuice Recipes)
* Version Number
* Author (Funky Waddle)
## View Recipe Screen
* Needs access to soft deleted flavors for recipes that currently use deleted flavors.
* Show components rounded to 1 decimal point
* Just convert percentage to decimal number and display, no need to validate sum(amounts) = Size (ml)
* We store the percent in the database, not the actual amount, so there is no check that the amount adds to size (ml), only that sum(percentages) = 100
* Shows recipe name,
* Amount in ml,
* PG: PG_ml, (rounded to 1 decimal point)
* VG: VG_ml, (rounded to 1 decimal point)
* Nic: Nic_ml, (rounded to 1 decimal point)
* Flavors
* If viewing individual flavors:
* Flavor 1 Name: Flavor_1_ml (rounded to 1 decimal point)
* Flavor 2 Name: Flavor_2_ml (rounded to 1 decimal point)
* if viewing Flavor Prep:
* Flavor Prep Amount
* Has button to open Edit Recipe Screen
* Has button to open Delete Recipe
* Has button to open Inventory Screen
* Has Drop Down for "View Recipe Using"
* Options are "Individual Flavors" or "Flavor Prep"
* When Flavor Prep is chosen, it replaces the list of flavors with a total ml of the sum of all the flavors in the recipe.
* When Individual Flavors is chosen (default), it replaces the Flavor Prep with the list of individual flavors.
* Has button for "Mix Flavor Prep"
* Flavor Prep is not stored in database. It's calculated on the fly based on flavors in the recipe.
* Shows recipe of Flavors only, no PG, VG, or Nicotine
* Flavor Prep is a batch of flavors
* Take ratio of each flavor to get total amount of each flavor (12% flavor 1 + 2% flavor 2 = 6:1 ratio)
* Flavor Prep batches of different sizes (10ml, 30ml, 120ml)
* Flavor Prep is just pre-mixing flavors, much like premixing flour and sugar for use in cookie batter.
* You can make 10ml or 1000ml of Sugar Flour, and it won't change the amount used in the cookie recipe.
* You can pre-mix 1000ml of flavors, and use 10ml per recipe, giving you 100 batches of Flavor Prep.
## Edit Recipe Screen
* Needs access to soft deleted flavors for recipes that currently use deleted flavors.
* Has a form for editing the recipe
* Name
* Size (mL) (10ml, 30ml, 120ml)
* PG Base %
* VG Base %
* Nic Base (PG or VG)
* Nic %
* Flavors
* Name (with company)
* Amount %
* If recipe contains a deleted flavor,
* Recipe can be saved with the deleted flavor still in the list, as long as it was not changed.
* Cannot actively change a flavor to a deleted flavor.
* Has button to save changes (Save form, then return to View Recipe screen)
* Has button to cancel changes (Discard form changes, return to View Recipe screen)
* Has button to delete recipe (Confirmation dialog)
* On Save, if recipe contains duplicate flavors,
* If duplicate flavor is found:
* Show question dialog to ask if user wants to "Combine the duplicate flavors" or "Cancel Save" and return to Edit Recipe screen.
* If user chooses to combine duplicate flavors,
* Combine flavors with same name and company into one flavor with combined percentage.
* If user chooses to cancel save,
* DO NOT SAVE and close dialog box, returning user to Edit Recipe screen.
* Validate that (PG Base + VG Base) === 100
* Validate that neither PG Base nor VG Base is < 0
* Validate sum(flavor.pct) + nic.pct + pg.pct + vg.pct ~= 100
* On App Exit, if on Edit Recipe screen and data has changed, ask to save changes before exiting.
* On Screen Change, if on Edit Recipe screen and data has changed, ask to save changes before changing screens.
## Add Recipe Screen
* Has a form for adding a recipe
* Name
* Size (mL) (10ml, 30ml, 120ml)
* PG Base %
* VG Base %
* Nic Base (PG or VG)
* Nic %
* Flavors
* Name
* Amount %
* Has button to save changes (Save form, then return to View Recipe screen)
* Has button to cancel changes (Discard form changes, return to View Recipe screen)
* On Save, if recipe contains duplicate flavors,
* If duplicate flavor is found:
* Show question dialog to ask if user wants to "Combine the duplicate flavors" or "Cancel Save" and return to Edit Recipe screen.
* If user chooses to combine duplicate flavors,
* Combine flavors with same name and company into one flavor with combined percentage.
* If user chooses to cancel save,
* DO NOT SAVE and close dialog box, returning user to Edit Recipe screen.
* Validate that (PG Base + VG Base) === 100
* Validate that neither PG Base nor VG Base is < 0
* Validate sum(flavor.pct) + nic.pct + pg.pct + vg.pct ~= 100
* On App Exit, if on Edit Recipe screen and data has changed, ask to save changes before exiting.
* On Screen Change, if on Edit Recipe screen and data has changed, ask to save changes before changing screens.
## Recipe Form Screens (Add/Edit)
* When adding/editing a recipe, you enter the PG, VG, Flavors, and Nic as %
* PG and VG are entered as % of total recipe size
* When changing PG or VG, the other is updated automatically so that PG + VG is always 100%
* Neither PG or VG can be negative
## Inventory Screen
* List of all Non-Deleted Flavors saved in the database
* Flavor Name
* Company
* PG or VG Base
* Has button to view Soft Deleted Flavors, and how many recipes they are in
* Can Hard Delete Soft Deleted Flavors if they are no longer in a recipe
* Can Un-Delete a Soft Deleted Flavor
* Each Flavor has a button to edit the flavor (Name, Company, PG or VG Base)
* Each Flavor has a button to delete the flavor
* When deleting a flavor,
* If flavor is in a recipe,
* Soft Delete the flavor
* Allow flavor data to be viewed in recipe, but with a strikethrough
* If flavor is not in a recipe,
* Hard Delete the flavor
* NO DUPLICATES ALLOWED

35
pyproject.toml Normal file
View file

@ -0,0 +1,35 @@
[project]
name = "funkyjuicerecipes"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"toga==0.5.3",
"peewee>=3.18.3"
]
[tool.uv]
environments = [
"sys_platform == 'linux'",
]
[tool.briefcase]
project_name = "FunkyJuice Recipes"
bundle = "com.TargonProducts"
version = "0.1.0"
url = "https://example.com"
author = "Funky Waddle"
author_email = "you@example.com"
license = "MIT"
[tool.briefcase.app.funkyjuicerecipes]
formal_name = "FunkyJuice Recipes"
description = "A python application for managing e-juice recipes"
sources = ["src/funkyjuicerecipes"]
requires = ["toga==0.5.3", "peewee>=3.18.3"]
icon = "src/funkyjuicerecipes/resources/icon"
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.setuptools.packages.find]
where = ["src"]

103
uv.lock Normal file
View file

@ -0,0 +1,103 @@
version = 1
revision = 2
requires-python = ">=3.10"
resolution-markers = [
"sys_platform == 'linux' and 'freebsd' in sys_platform",
"sys_platform == 'linux' and 'freebsd' not in sys_platform",
]
supported-markers = [
"sys_platform == 'linux'",
]
[[package]]
name = "funkyjuicerecipes"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "peewee", marker = "sys_platform == 'linux'" },
{ name = "toga", marker = "sys_platform == 'linux'" },
]
[package.metadata]
requires-dist = [
{ name = "peewee", specifier = ">=3.18.3" },
{ name = "toga", specifier = "==0.5.3" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "peewee"
version = "3.18.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/60/58e7a307a24044e0e982b99042fcd5a58d0cd928d9c01829574d7553ee8d/peewee-3.18.3.tar.gz", hash = "sha256:62c3d93315b1a909360c4b43c3a573b47557a1ec7a4583a71286df2a28d4b72e", size = 3026296, upload-time = "2025-11-03T16:43:46.678Z" }
[[package]]
name = "pycairo"
version = "1.29.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/22/d9/1728840a22a4ef8a8f479b9156aa2943cd98c3907accd3849fb0d5f82bfd/pycairo-1.29.0.tar.gz", hash = "sha256:f3f7fde97325cae80224c09f12564ef58d0d0f655da0e3b040f5807bd5bd3142", size = 665871, upload-time = "2025-11-11T19:13:01.584Z" }
[[package]]
name = "pygobject"
version = "3.54.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycairo", marker = "sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d3/a5/68f883df1d8442e3b267cb92105a4b2f0de819bd64ac9981c2d680d3f49f/pygobject-3.54.5.tar.gz", hash = "sha256:b6656f6348f5245606cf15ea48c384c7f05156c75ead206c1b246c80a22fb585", size = 1274658, upload-time = "2025-10-18T13:45:03.121Z" }
[[package]]
name = "toga"
version = "0.5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "toga-gtk", marker = "sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e8/b2/2a671f88bdc00234f86eafaf4990157a76f1252ba82027bf21a58924a1f3/toga-0.5.3.tar.gz", hash = "sha256:4dc555c1cdff459e1224f55e71cb5c44c98c04b665a6d4cc49a7d40c6b0fc071", size = 3899, upload-time = "2025-12-03T06:50:19.357Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/45/ceb4021e46a1dbad10a4101d61f34b1b1d5c8213bbf71071d4f17d27fa88/toga-0.5.3-py3-none-any.whl", hash = "sha256:d535e7829cfbff747349e1932e45b5492845da43998321bac81fd6a19c2f0f72", size = 3371, upload-time = "2025-12-03T06:50:07.774Z" },
]
[[package]]
name = "toga-core"
version = "0.5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "travertino", marker = "sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7b/65/02d6fc7572b09e100791b9053e3e47bb72c0ab97cd90a3ffb069e30fdcf1/toga_core-0.5.3.tar.gz", hash = "sha256:b8a32fb73f53088eec503bcf536e5b1da548147e356ae7c2077943c34af64519", size = 1023659, upload-time = "2025-12-03T06:50:18.656Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ac/83/7da1a3ca22d67b514ccaecc3342eb6cc2dc1a34852b8c29cc3199f4d2bfd/toga_core-0.5.3-py3-none-any.whl", hash = "sha256:6dc50df6575c18bf34f385167808b027122f16ee25f143907293362178c01fab", size = 147585, upload-time = "2025-12-03T06:50:06.837Z" },
]
[[package]]
name = "toga-gtk"
version = "0.5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging", marker = "sys_platform == 'linux'" },
{ name = "pycairo", marker = "sys_platform == 'linux'" },
{ name = "pygobject", marker = "sys_platform == 'linux'" },
{ name = "toga-core", marker = "sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/52/50/b76b8d74e43b5f9cd690e3a39c816dbfb75cab4209b696e6561c5f31f009/toga_gtk-0.5.3.tar.gz", hash = "sha256:38a566d14e90912c299d38e0a6e3a1ff6596a8db31412a9e37f4d23158b4a87f", size = 82525, upload-time = "2025-12-03T06:50:16.583Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/ca/1f65e926c4369b9a2d366498418b5d5e76b1dfe7b2448448869dc15de86e/toga_gtk-0.5.3-py3-none-any.whl", hash = "sha256:4e48a01b4aace7cd7e653bf82b2c31780e5b7f1d138130bb5ab73bc3f2bb5fa5", size = 75458, upload-time = "2025-12-03T06:50:05.718Z" },
]
[[package]]
name = "travertino"
version = "0.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c8/59/0940b241c3af6939a9c9fe22d218e77970b2fbeef7471dadafaeb3d59531/travertino-0.5.3.tar.gz", hash = "sha256:fdbdf3a2c78335ce6c357c334defe6f15e8315c0e213c43e59d1975e77df7cf3", size = 48923, upload-time = "2025-12-03T06:50:04.638Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/f0/dd7f32b7cb6e2102d5247150a57d74dc31fc09fea8a9212b7c2507e7df77/travertino-0.5.3-py3-none-any.whl", hash = "sha256:9f6f6840760af28295bba76aa175f35678aba33782ff29e78053b3cbdafafabf", size = 26118, upload-time = "2025-12-03T06:50:02.426Z" },
]