diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..ac36a0c --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,29 @@ +name: "Publish" + +on: + push: + branches: + - main + +jobs: + run: + name: "Build and publish release" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: uv.lock + + - name: Set up Python + run: uv python install 3.13 # Or whatever version you want to use. + + - name: Build + run: uv build + + - name: Publish + run: uv publish -t ${{ secrets.PYPI_TOKEN }} diff --git a/.gitignore b/.gitignore index 294c7ae..a33125f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ __pycache__/ *.db dist/ Bagels.egg-info/ -instance/ \ No newline at end of file +instance/ +snapshot_report.html \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7221a..ea1423a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.11 + +- Add transfers as templates. +- Fix: Crash when go to date in calendar +- Fix: Not creating and using custom config file + ## 0.1.10 - View by accounts is now an app action. Records will be filtered as well in that view. diff --git a/pyproject.toml b/pyproject.toml index 9f8040e..d079e39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "Bagels" -version = "0.1.10" +version = "0.1.11" authors = [ { name = "Jax", email = "enhancedjax@gmail.com" } ] @@ -23,7 +23,6 @@ dependencies = [ "frozenlist==1.5.0", "idna==3.10", "itsdangerous==2.2.0", - "jinja2==3.1.4", "linkify-it-py==2.0.3", "markdown-it-py==3.0.0", "markupsafe==3.0.2", @@ -39,7 +38,7 @@ dependencies = [ "pydantic-settings>=2.6.1", "pydantic==2.9.2", "pygments==2.18.0", - "pytest>=8.3.3", + "pytest-textual-snapshot>=1.0.0", "pyyaml==6.0.2", "rich==13.9.3", "sqlalchemy==2.0.36", @@ -70,6 +69,12 @@ bagels = "bagels.__main__:cli" [tool.uv] dev-dependencies = [ "textual-dev>=1.6.1", + "pytest>=8.3.1", + "jinja2>=3.1.4", + "syrupy>=4.6.1", + "pytest-xdist>=3.6.1", + "pytest-cov>=5.0.0", + "pytest-textual-snapshot>=1.0.0", ] [tool.hatch.metadata] diff --git a/src/bagels/__main__.py b/src/bagels/__main__.py index 637656e..5069840 100644 --- a/src/bagels/__main__.py +++ b/src/bagels/__main__.py @@ -1,25 +1,12 @@ from pathlib import Path +# from venv import create + import click -import yaml -from bagels.config import Config from bagels.locations import config_file, database_file, set_custom_root -def create_config_file() -> None: - f = config_file() - if f.exists(): - return - - try: - f.touch() - with open(f, "w") as f: - yaml.dump(Config.get_default().model_dump(), f) - except OSError: - pass - - @click.group(invoke_without_command=True) @click.option( "--at", @@ -33,6 +20,10 @@ def cli(ctx, at: click.Path | None): set_custom_root(at) if ctx.invoked_subcommand is None: + from bagels.config import load_config + + load_config() + from bagels.models.database.app import init_db init_db() diff --git a/src/bagels/app.py b/src/bagels/app.py index a4541e4..d9ee563 100644 --- a/src/bagels/app.py +++ b/src/bagels/app.py @@ -96,7 +96,13 @@ def watch_app_theme(self, theme: str | None) -> None: self.refresh_css(animate=False) self.screen._update_styles() if theme: - theme_object = self.themes[theme] + try: + theme_object = self.themes[theme] + except KeyError: + self.notify( + f"Theme {theme!r} not found.", title="Theme Error", timeout=1 + ) + return self.theme_change_signal.publish(theme_object) @on(CommandPalette.Opened) diff --git a/src/bagels/components/modules/datemode.py b/src/bagels/components/modules/datemode.py index 34d2bea..eb18dde 100644 --- a/src/bagels/components/modules/datemode.py +++ b/src/bagels/components/modules/datemode.py @@ -6,6 +6,7 @@ from textual.widgets import Label, Static from textual.widgets import Button +from bagels.forms.form import Form, FormField from bagels.modals.input import InputModal from bagels.config import CONFIG @@ -18,14 +19,16 @@ class DateMode(Static): Binding(CONFIG.hotkeys.home.datemode.go_to_day, "go_to_day", "Go to Day"), ] - FORM = [ - { - "type": "dateAutoDay", - "key": "date", - "title": "Date", - "default_value": datetime.now().strftime("%d"), - } - ] + FORM = Form( + fields=[ + FormField( + type="dateAutoDay", + key="date", + title="Date", + default_value=datetime.now().strftime("%d"), + ) + ] + ) def __init__(self, parent: Static, *args, **kwargs) -> None: super().__init__( diff --git a/src/bagels/components/modules/records.py b/src/bagels/components/modules/records.py index e18775a..977d586 100644 --- a/src/bagels/components/modules/records.py +++ b/src/bagels/components/modules/records.py @@ -481,7 +481,8 @@ def check_result_person(result) -> None: return if record.isTransfer: self.app.push_screen( - TransferModal(record), callback=check_result_records + TransferModal(title="Edit transfer", record=record), + callback=check_result_records, ) else: filled_form, filled_splits = self.record_form.get_filled_form( @@ -614,7 +615,7 @@ def check_result(result) -> None: timeout=3, ) - self.app.push_screen(TransferModal(), callback=check_result) + self.app.push_screen(TransferModal(title="New transfer"), callback=check_result) # region View # --------------- View --------------- # diff --git a/src/bagels/components/modules/templates.py b/src/bagels/components/modules/templates.py index 4f15ea5..e91aecb 100644 --- a/src/bagels/components/modules/templates.py +++ b/src/bagels/components/modules/templates.py @@ -7,12 +7,14 @@ from bagels.modals.confirmation import ConfirmationModal from bagels.modals.input import InputModal from bagels.config import CONFIG +from bagels.modals.transfer import TransferModal from bagels.models.record_template import RecordTemplate from bagels.managers.record_templates import ( create_template, delete_template, get_adjacent_template, get_all_templates, + get_template_by_id, swap_template_order, update_template, ) @@ -24,9 +26,12 @@ class Templates(Static): can_focus = True BINDINGS = [ - Binding(CONFIG.hotkeys.new, "new_template", "New Template"), - Binding(CONFIG.hotkeys.edit, "edit_template", "Edit Template"), - Binding(CONFIG.hotkeys.delete, "delete_template", "Delete Template"), + Binding(CONFIG.hotkeys.new, "new_template", "New"), + Binding( + CONFIG.hotkeys.home.new_transfer, "new_transfer", "New Transfer Template" + ), + Binding(CONFIG.hotkeys.edit, "edit_template", "Edit"), + Binding(CONFIG.hotkeys.delete, "delete_template", "Delete"), Binding("left", "swap_previous", "Swap left"), Binding("right", "swap_next", "Swap right"), ] @@ -56,15 +61,23 @@ def _create_templates_widgets(self, container: Container) -> None: for index, template in enumerate(self.templates): if index > 8: break - color = template.category.color - widget = Container( - Label( - f"[{color}]{CONFIG.symbols.category_color}[/{color}]", classes="dot" - ), - Label(f"{template.label}", classes="label"), - id=f"template-{template.id}", - classes="template-item", - ) + if template.isTransfer: + widget = Container( + Label(f"{template.label}", classes="label"), + id=f"template-{template.id}", + classes="template-item", + ) + else: + color = template.category.color + widget = Container( + Label( + f"[{color}]{CONFIG.symbols.category_color}[/{color}]", + classes="dot", + ), + Label(f"{template.label}", classes="label"), + id=f"template-{template.id}", + classes="template-item", + ) widget.border_subtitle = str(index + 1) widget.can_focus = True container.compose_add_child(widget) @@ -94,6 +107,7 @@ def _notify_no_selected_template(self) -> None: timeout=3, ) + # region Callback # ------------- Callback ------------- # def on_descendant_focus(self, event: events.DescendantFocus): @@ -123,16 +137,13 @@ def select_template(self, index: int) -> None: ) self.page_parent.rebuild() + # region CRUD + # ----------------- - ---------------- # + def action_new_template(self) -> None: def check_result(result) -> None: if result: create_template(result) - # try: - # except Exception as e: - # self.app.notify( - # title="Error", message=f"{e}", severity="error", timeout=10 - # ) - # else: self.app.notify( title="Success", message=f"Template created", @@ -146,6 +157,23 @@ def check_result(result) -> None: callback=check_result, ) + def action_new_transfer(self) -> None: + def check_result(result) -> None: + if result: + create_template(result) + self.app.notify( + title="Success", + message=f"Template created", + severity="information", + timeout=3, + ) + self.rebuild() + + self.app.push_screen( + TransferModal(title="New Transfer Template", isTemplate=True), + callback=check_result, + ) + def action_edit_template(self) -> None: if not self.selected_template_id: self._notify_no_selected_template() @@ -170,13 +198,24 @@ def check_result(result) -> None: self.rebuild() # ----------------- - ---------------- # - self.app.push_screen( - InputModal( - "Edit Template", - form=self.template_form.get_filled_form(self.selected_template_id), - ), - callback=check_result, - ) + template = get_template_by_id(self.selected_template_id) + if template.isTransfer: + self.app.push_screen( + TransferModal( + title="Edit Transfer Template", + record=template, + isTemplate=True, + ), + callback=check_result, + ) + else: + self.app.push_screen( + InputModal( + "Edit Template", + form=self.template_form.get_filled_form(self.selected_template_id), + ), + callback=check_result, + ) def action_delete_template(self) -> None: if not self.selected_template_id: diff --git a/src/bagels/config.py b/src/bagels/config.py index f0faf31..d57b4fc 100644 --- a/src/bagels/config.py +++ b/src/bagels/config.py @@ -2,7 +2,7 @@ from typing import Any, Literal import yaml from pydantic import BaseModel, Field -from pydantic_settings import BaseSettings, SettingsConfigDict +from pydantic_settings import SettingsConfigDict from pydantic_settings_yaml import YamlBaseSettings from bagels.locations import config_file @@ -63,7 +63,7 @@ class Symbols(BaseModel): class State(BaseModel): - theme: str = "posting" + theme: str = "dark" class Config(YamlBaseSettings): @@ -112,14 +112,23 @@ def get_default(cls): ) -# Only try to load from file if it exists -config_path = config_file() +CONFIG = None -with warnings.catch_warnings(): - warnings.simplefilter("ignore") - CONFIG = ( - Config.get_default() if not config_path.exists() else Config() - ) # ignore warnings about empty env file + +def load_config(): + f = config_file() + if not f.exists(): + try: + f.touch() + with open(f, "w") as f: + yaml.dump(Config.get_default().model_dump(), f) + except OSError: + pass + + global CONFIG + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + CONFIG = Config() # ignore warnings about empty env file def write_state(key: str, value: Any) -> None: diff --git a/src/bagels/modals/transfer.py b/src/bagels/modals/transfer.py index 265afbf..bb5613f 100644 --- a/src/bagels/modals/transfer.py +++ b/src/bagels/modals/transfer.py @@ -48,7 +48,7 @@ def __init__(self, accounts, initial_id: int = 0, type: str = "", *args, **kwarg class TransferModal(ModalScreen): - def __init__(self, record=None, *args, **kwargs): + def __init__(self, title="", record=None, isTemplate=False, *args, **kwargs): super().__init__(classes="modal-screen", *args, **kwargs) self.accounts = get_all_accounts_with_balance(get_hidden=True) self.form = Form( @@ -70,6 +70,10 @@ def __init__(self, record=None, *args, **kwargs): is_required=True, default_value=str(record.amount) if record else "", ), + ] + ) + if not isTemplate: + self.form.fields.append( FormField( title="Date", key="date", @@ -80,15 +84,11 @@ def __init__(self, record=None, *args, **kwargs): if record else datetime.now().strftime("%d") ), - ), - ] - ) + ) + ) self.fromAccount = record.accountId if record else self.accounts[0].id self.toAccount = record.transferToAccountId if record else self.accounts[1].id - if record: - self.title = "Edit transfer" - else: - self.title = "New transfer" + self.title = title self.atAccountList = False def on_descendant_focus(self, event: events.DescendantFocus): diff --git a/tests/__snapshots__/snapshot/test_loads.svg b/tests/__snapshots__/snapshot/test_loads.svg new file mode 100644 index 0000000..c254c30 --- /dev/null +++ b/tests/__snapshots__/snapshot/test_loads.svg @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + App + + + + + + + + + + +↪ Bagels0.1.10jax@Jaxs-Air + +╭─ Accounts (using) ───╮╭─ View and add ─────────────╮╭─ Templates ────────────────────────────────────────────────────────────────────╮ +││ Expense  Income ││╭─────────────╮╭────────────────╮╭────────────────────────╮ +Bank      4703.52│╰──────────────────────── / ─╯│Home->Uni││Monthly Rent││Netflix Subscription +asd│╭─ Period ───────────────────╮│╰───────── 1 ─╯╰──────────── 2 ─╯╰──────────────────── 3 ─╯ +││ <<<      Today       >>> │╰──────────────────────────────────────────────────────────────────────── 1 - 9 ─╯ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔││╭─ Records ──────────────────────────────────────────────────────────────────────╮ +Card          80.0││SMTWTFS╭─ Display by: ──────────────────────────────────────────────────────────────╮ +││272829303112 Date  Person  +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔││3456789╰────────────────────────────────────────────────────────────────────── q w ─╯ +Test        -123.0││10111213141516 Category  Amount  Label  Account  +qwe││17181920212223╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +││24252627282930╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +││╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╰──────────────── [ ] ─╯╰──────────────────── ← . → ─╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╭─ Insights ─────────────────────────────────────────╮╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +Expense of Today          Expense per day╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱Noentries╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +0                      0.0╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱No data to display╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╰──────────────────────────────────────────────── \ ─╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ +╰────────────────────────────────────────────────────────────────────────────────╯ + + + + + a Add  d Delete  e Edit  t Transfer  s Toggle Splits  \ Use account  v Jump Mode  c Categories  ^q Quit                        ^p palette + + + diff --git a/tests/snapshot.py b/tests/snapshot.py new file mode 100644 index 0000000..2d62f85 --- /dev/null +++ b/tests/snapshot.py @@ -0,0 +1,12 @@ +import pytest +from textual.pilot import Pilot + +from bagels.locations import set_custom_root + +APP_PATH = "../src/bagels/app.py" + +set_custom_root("./instance/") + + +def test_loads(snap_compare): + assert snap_compare(APP_PATH, terminal_size=(140, 40)) diff --git a/uv.lock b/uv.lock index 5d22ca8..aa38986 100644 --- a/uv.lock +++ b/uv.lock @@ -86,7 +86,7 @@ wheels = [ [[package]] name = "bagels" -version = "0.1.10" +version = "0.1.11" source = { editable = "." } dependencies = [ { name = "aiohappyeyeballs" }, @@ -102,7 +102,6 @@ dependencies = [ { name = "frozenlist" }, { name = "idna" }, { name = "itsdangerous" }, - { name = "jinja2" }, { name = "linkify-it-py" }, { name = "markdown-it-py" }, { name = "markupsafe" }, @@ -118,7 +117,7 @@ dependencies = [ { name = "pydantic-settings" }, { name = "pydantic-settings-yaml" }, { name = "pygments" }, - { name = "pytest" }, + { name = "pytest-textual-snapshot" }, { name = "pyyaml" }, { name = "rich" }, { name = "sqlalchemy" }, @@ -134,6 +133,12 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "jinja2" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-textual-snapshot" }, + { name = "pytest-xdist" }, + { name = "syrupy" }, { name = "textual-dev" }, ] @@ -152,7 +157,6 @@ requires-dist = [ { name = "frozenlist", specifier = "==1.5.0" }, { name = "idna", specifier = "==3.10" }, { name = "itsdangerous", specifier = "==2.2.0" }, - { name = "jinja2", specifier = "==3.1.4" }, { name = "linkify-it-py", specifier = "==2.0.3" }, { name = "markdown-it-py", specifier = "==3.0.0" }, { name = "markupsafe", specifier = "==3.0.2" }, @@ -168,7 +172,7 @@ requires-dist = [ { name = "pydantic-settings", specifier = ">=2.6.1" }, { name = "pydantic-settings-yaml", specifier = ">=0.2.0" }, { name = "pygments", specifier = "==2.18.0" }, - { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-textual-snapshot", specifier = ">=1.0.0" }, { name = "pyyaml", specifier = "==6.0.2" }, { name = "rich", specifier = "==13.9.3" }, { name = "sqlalchemy", specifier = "==2.0.36" }, @@ -183,7 +187,15 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "textual-dev", specifier = ">=1.6.1" }] +dev = [ + { name = "jinja2", specifier = ">=3.1.4" }, + { name = "pytest", specifier = ">=8.3.1" }, + { name = "pytest-cov", specifier = ">=5.0.0" }, + { name = "pytest-textual-snapshot", specifier = ">=1.0.0" }, + { name = "pytest-xdist", specifier = ">=3.6.1" }, + { name = "syrupy", specifier = ">=4.6.1" }, + { name = "textual-dev", specifier = ">=1.6.1" }, +] [[package]] name = "blinker" @@ -227,6 +239,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "coverage" +version = "7.6.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/68/26895f8b068e384b1ec9ab122565b913b735e6b4c618b3d265a280607edc/coverage-7.6.7.tar.gz", hash = "sha256:d79d4826e41441c9a118ff045e4bccb9fdbdcb1d02413e7ea6eb5c87b5439d24", size = 799938 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/87/c590d0c7eeb884995d9d06b429c5e88e9fcd65d3a6a686d9476cb50b72a9/coverage-7.6.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46f21663e358beae6b368429ffadf14ed0a329996248a847a4322fb2e35d64d3", size = 207199 }, + { url = "https://files.pythonhosted.org/packages/40/ee/c88473c4f69c952f4425fabe045cb78d2027634ce50c9d7f7987d389b604/coverage-7.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40cca284c7c310d622a1677f105e8507441d1bb7c226f41978ba7c86979609ab", size = 207454 }, + { url = "https://files.pythonhosted.org/packages/b8/07/afda6e10c50e3a8c21020c5c1d1b4f3d7eff1c190305cef2962adf8de018/coverage-7.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77256ad2345c29fe59ae861aa11cfc74579c88d4e8dbf121cbe46b8e32aec808", size = 239971 }, + { url = "https://files.pythonhosted.org/packages/85/43/bd1934b75e31f2a49665be6a6b7f8bfaff7266ba19721bdb90239f5e9ed7/coverage-7.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87ea64b9fa52bf395272e54020537990a28078478167ade6c61da7ac04dc14bc", size = 237119 }, + { url = "https://files.pythonhosted.org/packages/2b/19/7a70458c1624724086195b40628e91bc5b9ca180cdfefcc778285c49c7b2/coverage-7.6.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d608a7808793e3615e54e9267519351c3ae204a6d85764d8337bd95993581a8", size = 239109 }, + { url = "https://files.pythonhosted.org/packages/f3/2c/3dee671415ff13c05ca68243b2264fc95a5eea57697cffa7986b68b8f608/coverage-7.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdd94501d65adc5c24f8a1a0eda110452ba62b3f4aeaba01e021c1ed9cb8f34a", size = 238769 }, + { url = "https://files.pythonhosted.org/packages/37/ad/e0d1228638711aeacacc98d1197af6226b6d062d12c81a6bcc17d3234533/coverage-7.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82c809a62e953867cf57e0548c2b8464207f5f3a6ff0e1e961683e79b89f2c55", size = 236854 }, + { url = "https://files.pythonhosted.org/packages/90/95/6467e9d9765a63c7f142703a7f212f6af114bd73a6c1cffeb7ad7f003a86/coverage-7.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb684694e99d0b791a43e9fc0fa58efc15ec357ac48d25b619f207c41f2fd384", size = 238701 }, + { url = "https://files.pythonhosted.org/packages/b2/7a/fc11a163f0fd6ce8539d0f1b565873fe6903b900214ff71b5d80d16154c3/coverage-7.6.7-cp313-cp313-win32.whl", hash = "sha256:963e4a08cbb0af6623e61492c0ec4c0ec5c5cf74db5f6564f98248d27ee57d30", size = 209865 }, + { url = "https://files.pythonhosted.org/packages/f2/91/58be3a56efff0c3481e48e2caa56d5d6f3c5c8d385bf4adbecdfd85484b0/coverage-7.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:14045b8bfd5909196a90da145a37f9d335a5d988a83db34e80f41e965fb7cb42", size = 210597 }, + { url = "https://files.pythonhosted.org/packages/34/7e/fed983809c2eccb09c5ddccfdb08efb7f2dd1ae3454dabf1c92c5a2e9946/coverage-7.6.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f2c7a045eef561e9544359a0bf5784b44e55cefc7261a20e730baa9220c83413", size = 207944 }, + { url = "https://files.pythonhosted.org/packages/c7/e0/2c1a157986a3927c3920e8e3938a3fdf33ea22b6f371dc3b679f13f619e2/coverage-7.6.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dd4e4a49d9c72a38d18d641135d2fb0bdf7b726ca60a103836b3d00a1182acd", size = 208215 }, + { url = "https://files.pythonhosted.org/packages/35/2f/77b086b228f6443ae5499467d1629c7428925b390cd171350c403bc00f14/coverage-7.6.7-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c95e0fa3d1547cb6f021ab72f5c23402da2358beec0a8e6d19a368bd7b0fb37", size = 250930 }, + { url = "https://files.pythonhosted.org/packages/60/d8/2ffea937d89ee328fc6e47c2515b890735bdf3f195d507d1c78b5fa96939/coverage-7.6.7-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f63e21ed474edd23f7501f89b53280014436e383a14b9bd77a648366c81dce7b", size = 246647 }, + { url = "https://files.pythonhosted.org/packages/b2/81/efbb3b00a7f7eb5f54a3b3b9f19b26d770a0b7d3870d651f07d2451c5504/coverage-7.6.7-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead9b9605c54d15be228687552916c89c9683c215370c4a44f1f217d2adcc34d", size = 249006 }, + { url = "https://files.pythonhosted.org/packages/eb/91/ce36990cbefaf7909e96c888ed4d83f3471fc1be3273a5beda10896cde0f/coverage-7.6.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0573f5cbf39114270842d01872952d301027d2d6e2d84013f30966313cadb529", size = 248500 }, + { url = "https://files.pythonhosted.org/packages/75/3f/b8c87dfdd96276870fb4abc7e2957cba7d20d8a435fcd816d807869ec833/coverage-7.6.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e2c8e3384c12dfa19fa9a52f23eb091a8fad93b5b81a41b14c17c78e23dd1d8b", size = 246388 }, + { url = "https://files.pythonhosted.org/packages/a0/51/62273e1d5c25bb8fbef5fbbadc75b4a3e08c11b80516d0a97c25e5cced5b/coverage-7.6.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:70a56a2ec1869e6e9fa69ef6b76b1a8a7ef709972b9cc473f9ce9d26b5997ce3", size = 247669 }, + { url = "https://files.pythonhosted.org/packages/75/e5/d7772e56a7eace80e98ac39f2756d4b690fc0ce2384418174e02519a26a8/coverage-7.6.7-cp313-cp313t-win32.whl", hash = "sha256:dbba8210f5067398b2c4d96b4e64d8fb943644d5eb70be0d989067c8ca40c0f8", size = 210510 }, + { url = "https://files.pythonhosted.org/packages/2d/12/f2666e4e36b43221391ffcd971ab0c50e19439c521c2c87cd7e0b49ddba2/coverage-7.6.7-cp313-cp313t-win_amd64.whl", hash = "sha256:dfd14bcae0c94004baba5184d1c935ae0d1231b8409eb6c103a5fd75e8ecdc56", size = 211660 }, +] + +[[package]] +name = "execnet" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612 }, +] + [[package]] name = "freezegun" version = "1.5.1" @@ -593,6 +642,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, ] +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "pytest-textual-snapshot" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "pytest" }, + { name = "rich" }, + { name = "syrupy" }, + { name = "textual" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/75/2ef17ae52fa5bc848ff2d1d7bc317a702cbd6d7ad733ca991b9f899dbbae/pytest_textual_snapshot-1.0.0.tar.gz", hash = "sha256:065217055ed833b8a16f2320a0613f39a0154e8d9fee63535f29f32c6414b9d7", size = 11071 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/2e/4bf16ed78b382b3d7c1e545475ec8cf04346870be662815540faf8f16e8c/pytest_textual_snapshot-1.0.0-py3-none-any.whl", hash = "sha256:dd3a421491a6b1987ee7b4336d7f65299524924d2b0a297e69733b73b01570e1", size = 11171 }, +] + +[[package]] +name = "pytest-xdist" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/c4/3c310a19bc1f1e9ef50075582652673ef2bfc8cd62afef9585683821902f/pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d", size = 84060 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/82/1d96bf03ee4c0fdc3c0cbe61470070e659ca78dc0086fb88b66c185e2449/pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7", size = 46108 }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -673,6 +764,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/49/21633706dd6feb14cd3f7935fc00b60870ea057686035e1a99ae6d9d9d53/SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", size = 1883787 }, ] +[[package]] +name = "syrupy" +version = "4.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/81/f46d234fa4ca0edcdeed973bab9acd8f8ac186537cdc850e9e84a00f61a0/syrupy-4.7.2.tar.gz", hash = "sha256:ea45e099f242de1bb53018c238f408a5bb6c82007bc687aefcbeaa0e1c2e935a", size = 49320 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/75/57b629fdd256efc58fb045618d603ce0b0f5fcc477f34b758e34423efb99/syrupy-4.7.2-py3-none-any.whl", hash = "sha256:eae7ba6be5aed190237caa93be288e97ca1eec5ca58760e4818972a10c4acc64", size = 49234 }, +] + [[package]] name = "textual" version = "0.86.1"