From b340170de2c91a41f6ce6654320a4515e88f39ea Mon Sep 17 00:00:00 2001 From: Rebot <96078724+reboot-dev-bot@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:29:33 +0000 Subject: [PATCH] [Release] Synchronize for release --- .gitignore | 3 + .tests/all_pytests.sh | 2 +- api/bank/v1/bank.proto | 3 +- bank/backend/tests/account.feature | 24 ++++ bank/backend/tests/account_servicer_test.py | 108 +++------------- bank/backend/tests/bank.feature | 23 ++++ bank/backend/tests/bank_servicer_test.py | 84 ++---------- .../backend/tests/hello.feature | 11 ++ .../backend/tests/hello_servicer_test.py | 38 ++---- hello-tasks/backend/tests/hello.feature | 15 +++ .../backend/tests/hello_servicer_test.py | 61 +++------ pyproject.toml | 2 +- uv.lock | 122 +++++++++++++++--- 13 files changed, 241 insertions(+), 255 deletions(-) create mode 100644 bank/backend/tests/account.feature create mode 100644 bank/backend/tests/bank.feature create mode 100644 hello-constructors/backend/tests/hello.feature create mode 100644 hello-tasks/backend/tests/hello.feature diff --git a/.gitignore b/.gitignore index 6679fc3..be074a8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ hello-tasks/backend/api # Temporary directories produced by `pytest`. __pycache__/ .pytest_cache/ + +# Recordings of browser scenarios, made by running the tests. +*.recordings/ diff --git a/.tests/all_pytests.sh b/.tests/all_pytests.sh index 7a4f59b..9e06fe4 100755 --- a/.tests/all_pytests.sh +++ b/.tests/all_pytests.sh @@ -36,7 +36,7 @@ function runPyTest () { # Install the `reboot` package from the specified path explicitly, over- # writing the version from `pyproject.toml`. -uv add --no-sync "${SANDBOX_ROOT}$REBOOT_WHL_FILE" +uv add --no-sync "reboot[dev] @ ${SANDBOX_ROOT}$REBOOT_WHL_FILE" # Force a fresh virtualenv. A pre-existing `.venv/` (e.g., carried # over from a pre-baked image, or copied between containers/host diff --git a/api/bank/v1/bank.proto b/api/bank/v1/bank.proto index 5c59f60..c2151b6 100644 --- a/api/bank/v1/bank.proto +++ b/api/bank/v1/bank.proto @@ -11,12 +11,13 @@ import "bank/v1/errors.proto"; service BankMethods { rpc SignUp(SignUpRequest) returns (SignUpResponse) { option (rbt.v1alpha1.method).transaction = { + exclusive: {}, }; } rpc Transfer(TransferRequest) returns (TransferResponse) { option (rbt.v1alpha1.method) = { - transaction: {}, + transaction: { shared: {} }, errors: [ "OverdraftError" ], }; } diff --git a/bank/backend/tests/account.feature b/bank/backend/tests/account.feature new file mode 100644 index 0000000..f60fb19 --- /dev/null +++ b/bank/backend/tests/account.feature @@ -0,0 +1,24 @@ +Feature: Accounts + + Background: + Given the application is up + And "anonymous" is an unauthenticated user + + Scenario: Depositing and withdrawing move the balance + Given "anonymous" creates an `Account` via `open` with `customer_name="Alice"` + And the resulting state id is saved as "account_id" + Then as "anonymous", `balance` on the `Account` for "" has `balance=0` + When "anonymous" does a `deposit` with `amount=100` on `Account` of "" + Then as "anonymous", `balance` on the `Account` for "" has `balance=100` + When "anonymous" does a `withdraw` with `amount=60` on `Account` of "" + Then as "anonymous", `balance` on the `Account` for "" has `balance=40` + When "anonymous" attempts a `withdraw` with `amount=65` on `Account` of "" + Then the attempt aborts with `OverdraftError` with `amount=25` + And as "anonymous", `balance` on the `Account` for "" has `balance=40` + + Scenario: Opening sends a welcome email + Given "anonymous" creates an `Account` via `open` with `customer_name="Bob"` + And the resulting state id is saved as "account_id" + And the resulting `welcome_email_task_id` is saved as "welcome_email_task_id" + Then "anonymous" awaits the `welcome_email` task "" on `Account` within 30 seconds + And the welcome email was sent diff --git a/bank/backend/tests/account_servicer_test.py b/bank/backend/tests/account_servicer_test.py index ddb89ac..12d6094 100644 --- a/bank/backend/tests/account_servicer_test.py +++ b/bank/backend/tests/account_servicer_test.py @@ -1,99 +1,31 @@ -import unittest +"""The account's tests: the Gherkin scenarios in `account.feature`.""" + +import pytest from account_servicer import AccountServicer -from bank.v1.account_rbt import Account, BalanceResponse -from bank.v1.errors_pb2 import OverdraftError from reboot.aio.applications import Application -from reboot.aio.tests import Reboot +from reboot.bdd import scenarios, then +from typing import Iterator from unittest import mock -def report_error_to_user(error_message: str) -> None: - # This is a dummy function for use in documentation code snippets. - pass - - -class TestAccount(unittest.IsolatedAsyncioTestCase): - - async def asyncSetUp(self) -> None: - self.rbt = Reboot() - await self.rbt.start() - - async def asyncTearDown(self) -> None: - await self.rbt.stop() - - async def test_basics(self) -> None: - await self.rbt.up(Application(servicers=[AccountServicer])) - context = self.rbt.create_external_context(name=f"test-{self.id()}") - - # Create the state machine by calling its constructor. The fact that the - # state machine _has_ a constructor means that this step is required - # before other methods can be called on it. - account, _ = await Account.open(context, customer_name="Alice") - - # We can now call methods on the state machine. It should have a balance - # of 0. - response: BalanceResponse = await account.balance(context) - self.assertEqual(response.balance, 0) - - # When we deposit money, the balance should go up. - await account.deposit(context, amount=100) - response = await account.balance(context) - self.assertEqual(response.balance, 100) - - # When we withdraw money, the balance should go down. - await account.withdraw(context, amount=60) - response = await account.balance(context) - self.assertEqual(response.balance, 40) - - # When we withdraw too much money, we should get an error. - # Use a helper function here to get a code snippet for use in docs. - async def withdraw(): - try: - await account.withdraw(context, amount=65) - except Account.WithdrawAborted as aborted: - match aborted.error: - case OverdraftError(amount=amount): - report_error_to_user( - 'Your withdrawal could not be processed due to ' - 'insufficient funds. Your account balance is less ' - f'than the requested amount by {amount} dollars.' - ) - raise +@pytest.fixture +def application() -> Application: + return Application(servicers=[AccountServicer]) - with self.assertRaises(Account.WithdrawAborted) as aborted: - await withdraw() - self.assertTrue(isinstance(aborted.exception.error, OverdraftError)) - self.assertEqual(aborted.exception.error.amount, 25) - # ... and the balance shouldn't have changed. - response = await account.balance(context) - self.assertEqual(response.balance, 40) +# The welcome email goes through `send_email`, mocked so scenarios +# can observe it. +@pytest.fixture(autouse=True) +def send_email() -> Iterator[mock.AsyncMock]: + with mock.patch('account_servicer.send_email') as mocked: + yield mocked - @mock.patch("account_servicer.send_email") - async def test_send_welcome_email(self, mock_send_email) -> None: - await self.rbt.up( - Application(servicers=[AccountServicer]), - ) - context = self.rbt.create_external_context(name=f"test-{self.id()}") - # When we open an account, we expect the user to receive a welcome - # email. - account, open_response = await Account.open( - context, - customer_name="Alice", - ) +@then('the welcome email was sent') +def _the_welcome_email_was_sent(send_email: mock.AsyncMock) -> None: + # Reboot re-runs methods twice in development mode to validate + # that they are idempotent, so the email sends twice. + assert send_email.call_count == 2 - welcome_email_task_id = open_response.welcome_email_task_id - # Wait for the email task to run. - response = await Account.WelcomeEmailTask.retrieve( - context, - task_id=welcome_email_task_id, - ) - # We are only capturing the response for docs purposes, and - # need to explicitly delete it to avoid linting errors. - del response - # We can expect two attempts to send the email, because Reboot always - # re-runs methods twice in development mode in order to validate that - # calls are idempotent. - self.assertEqual(mock_send_email.call_count, 2) +scenarios('account.feature') diff --git a/bank/backend/tests/bank.feature b/bank/backend/tests/bank.feature new file mode 100644 index 0000000..ee92fcc --- /dev/null +++ b/bank/backend/tests/bank.feature @@ -0,0 +1,23 @@ +Feature: Bank + + Background: + Given the application is up + And "anonymous" is an unauthenticated user + + Scenario: Signing up opens an account + When "anonymous" does a `sign_up` with `customer_name="Alice"` on `Bank` of "my-bank" + And the resulting `account_id` is saved as "alice_account_id" + Then as "anonymous", `balance` on the `Account` for "" has `balance=0` + + Scenario: Transfers move money between accounts + Given "anonymous" does a `sign_up` with `customer_name="Alice"` on `Bank` of "my-bank" + And the resulting `account_id` is saved as "alice_account_id" + And "anonymous" does a `sign_up` with `customer_name="Bob"` on `Bank` of "my-bank" + And the resulting `account_id` is saved as "bob_account_id" + When "anonymous" does a `deposit` with `amount=100` on `Account` of "" + Then as "anonymous", `balance` on the `Account` for "" has `balance=100` + When "anonymous" does a `transfer` with `from_account_id=` and `to_account_id=` and `amount=40` on `Bank` of "my-bank" + Then as "anonymous", `balance` on the `Account` for "" has `balance=60` + And as "anonymous", `balance` on the `Account` for "" has `balance=40` + When "anonymous" attempts a `transfer` with `from_account_id=` and `to_account_id=` and `amount=50` on `Bank` of "my-bank" + Then the attempt aborts with `OverdraftError` with `amount=10` diff --git a/bank/backend/tests/bank_servicer_test.py b/bank/backend/tests/bank_servicer_test.py index b37ed90..b8446f6 100644 --- a/bank/backend/tests/bank_servicer_test.py +++ b/bank/backend/tests/bank_servicer_test.py @@ -1,83 +1,15 @@ -import unittest +"""The bank's tests: the Gherkin scenarios in `bank.feature`.""" + +import pytest from account_servicer import AccountServicer -from bank.v1.account_rbt import Account, BalanceResponse -from bank.v1.bank_rbt import Bank, SignUpResponse -from bank.v1.errors_pb2 import OverdraftError from bank_servicer import BankServicer from reboot.aio.applications import Application -from reboot.aio.tests import Reboot - - -class TestAccount(unittest.IsolatedAsyncioTestCase): - - async def asyncSetUp(self) -> None: - self.rbt = Reboot() - await self.rbt.start() - - async def asyncTearDown(self) -> None: - await self.rbt.stop() - - async def test_signup(self) -> None: - await self.rbt.up( - Application(servicers=[BankServicer, AccountServicer]) - ) - context = self.rbt.create_external_context(name=f"test-{self.id()}") - bank = Bank.ref("my-bank") - - # The Bank state machine doesn't have a constructor, so we can simply - # start calling methods on it. - response: SignUpResponse = await bank.sign_up( - context, - customer_name="Alice", - ) - - # SignUp will have created an Account we can call. - account = Account.ref(response.account_id) - response = await account.balance(context) - self.assertEqual(response.balance, 0) - - async def test_transfer(self): - await self.rbt.up( - Application(servicers=[BankServicer, AccountServicer]) - ) - context = self.rbt.create_external_context(name=f"test-{self.id()}") - bank = Bank.ref("my-bank") +from reboot.bdd import scenarios - alice: SignUpResponse = await bank.sign_up( - context, - customer_name="Alice", - ) - alice_account = Account.ref(alice.account_id) - bob: SignUpResponse = await bank.sign_up( - context, - customer_name="Bob", - ) - bob_account = Account.ref(bob.account_id) - # Alice deposits some money. - await alice_account.deposit(context, amount=100) - response: BalanceResponse = await alice_account.balance(context) - self.assertEqual(response.balance, 100) +@pytest.fixture +def application() -> Application: + return Application(servicers=[BankServicer, AccountServicer]) - # Alice transfers some money to Bob. - await bank.transfer( - context, - from_account_id=alice.account_id, - to_account_id=bob.account_id, - amount=40, - ) - response = await alice_account.balance(context) - self.assertEqual(response.balance, 60) - response = await bob_account.balance(context) - self.assertEqual(response.balance, 40) - # Bob tries to transfer too much money back to Alice. - with self.assertRaises(Bank.TransferAborted) as aborted: - await bank.transfer( - context, - from_account_id=bob.account_id, - to_account_id=alice.account_id, - amount=50, - ) - self.assertTrue(isinstance(aborted.exception.error, OverdraftError)) - self.assertEqual(aborted.exception.error.amount, 10) +scenarios('bank.feature') diff --git a/hello-constructors/backend/tests/hello.feature b/hello-constructors/backend/tests/hello.feature new file mode 100644 index 0000000..14f3149 --- /dev/null +++ b/hello-constructors/backend/tests/hello.feature @@ -0,0 +1,11 @@ +Feature: Hello with a factory + + Background: + Given the application is up + And "anonymous" is an unauthenticated user + + Scenario: Messages record from creation onward + Given "anonymous" creates a `Hello` via `create` with `initial_message="first message"` + And the resulting state id is saved as "hello_id" + When "anonymous" does a `send` with `message="second message"` on `Hello` of "" + Then as "anonymous", `messages` on the `Hello` for "" has `messages=["first message", "second message"]` diff --git a/hello-constructors/backend/tests/hello_servicer_test.py b/hello-constructors/backend/tests/hello_servicer_test.py index c982966..b851fe3 100644 --- a/hello-constructors/backend/tests/hello_servicer_test.py +++ b/hello-constructors/backend/tests/hello_servicer_test.py @@ -1,36 +1,14 @@ -import unittest -from hello_constructors.v1.hello_rbt import Hello +"""Hello's tests: the Gherkin scenarios in `hello.feature`.""" + +import pytest from hello_servicer import HelloServicer from reboot.aio.applications import Application -from reboot.aio.tests import Reboot - - -class TestHello(unittest.IsolatedAsyncioTestCase): - - async def asyncSetUp(self) -> None: - self.rbt = Reboot() - await self.rbt.start() - - async def asyncTearDown(self) -> None: - await self.rbt.stop() - - async def test_hello_constructors(self) -> None: - await self.rbt.up(Application(servicers=[HelloServicer])) +from reboot.bdd import scenarios - context = self.rbt.create_external_context(name=f"test-{self.id()}") - # Create the state machine by calling its constructor. The fact that the - # state machine _has_ a constructor means that this step is required - # before other methods can be called on it. - hello, _ = await Hello.create(context, initial_message="first message") +@pytest.fixture +def application() -> Application: + return Application(servicers=[HelloServicer]) - # Send another message. - await hello.send(context, message="second message") - messages_response = await hello.messages(context) - self.assertEqual( - messages_response.messages, [ - "first message", - "second message", - ] - ) +scenarios('hello.feature') diff --git a/hello-tasks/backend/tests/hello.feature b/hello-tasks/backend/tests/hello.feature new file mode 100644 index 0000000..afd22ee --- /dev/null +++ b/hello-tasks/backend/tests/hello.feature @@ -0,0 +1,15 @@ +Feature: Hello with tasks + + Background: + Given the application is up + And "anonymous" is an unauthenticated user + + Scenario: Sent messages get a warning and then erase + When "anonymous" does a `send` with `message="Hello, World!"` on `Hello` of "testing-hello" + And the resulting `task_id` is saved as "warning_task_id" + # A completed task's response is the result, so the erase task's + # ID saves from it the way any response property does. + When "anonymous" awaits the `warning` task "" on `Hello` within 30 seconds + And the resulting `task_id` is saved as "erase_task_id" + And "anonymous" awaits the `erase` task "" on `Hello` within 30 seconds + Then as "anonymous", `messages` on the `Hello` for "testing-hello" has `messages` of length `1` and `messages[0]="Number of messages erased so far: 1"` diff --git a/hello-tasks/backend/tests/hello_servicer_test.py b/hello-tasks/backend/tests/hello_servicer_test.py index 4d9dd77..7a2822b 100644 --- a/hello-tasks/backend/tests/hello_servicer_test.py +++ b/hello-tasks/backend/tests/hello_servicer_test.py @@ -1,50 +1,29 @@ +"""Hello's tests: the Gherkin scenarios in `hello.feature`.""" + import hello_servicer -import unittest +import pytest from hello_servicer import HelloServicer -from hello_tasks.v1.hello_rbt import Hello from reboot.aio.applications import Application -from reboot.aio.tests import Reboot - - -class TestHello(unittest.IsolatedAsyncioTestCase): - - async def asyncSetUp(self) -> None: - self.rbt = Reboot() - await self.rbt.start() - - async def asyncTearDown(self) -> None: - await self.rbt.stop() +from reboot.bdd import scenarios +from typing import Iterator - async def test_hello_tasks(self) -> None: - # To make our test run quickly, remove delays before erasing the - # message. - hello_servicer.SECS_UNTIL_WARNING = 0 - hello_servicer.ADDITIONAL_SECS_UNTIL_ERASE = 0 - await self.rbt.up( - Application(servicers=[HelloServicer]), - ) - context = self.rbt.create_external_context(name=f"test-{self.id()}") +# To make scenarios run quickly, remove the delays before warning +# about and before erasing a message. +@pytest.fixture(autouse=True) +def no_delays() -> Iterator[None]: + secs_until_warning = hello_servicer.SECS_UNTIL_WARNING + additional_secs_until_erase = hello_servicer.ADDITIONAL_SECS_UNTIL_ERASE + hello_servicer.SECS_UNTIL_WARNING = 0 + hello_servicer.ADDITIONAL_SECS_UNTIL_ERASE = 0 + yield + hello_servicer.SECS_UNTIL_WARNING = secs_until_warning + hello_servicer.ADDITIONAL_SECS_UNTIL_ERASE = additional_secs_until_erase - hello = Hello.ref("testing-hello") - # Send a message. - send_response = await hello.send(context, message="Hello, World!") +@pytest.fixture +def application() -> Application: + return Application(servicers=[HelloServicer]) - # Wait for the message to be erased. - warning_response = await Hello.WarningTask.retrieve( - context, - task_id=send_response.task_id, - ) - await Hello.EraseTask.retrieve( - context, - task_id=warning_response.task_id, - ) - # Check that the current list of messages reflects the erasure. - messages_response = await hello.messages(context) - self.assertEqual(len(messages_response.messages), 1) - self.assertEqual( - messages_response.messages[0], - "Number of messages erased so far: 1", - ) +scenarios('hello.feature') diff --git a/pyproject.toml b/pyproject.toml index fadb0c9..33ea187 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "monorepo" version = "0.1.0" requires-python = ">= 3.10" dependencies = [ - "reboot==1.5.0", + "reboot[dev]==1.6.0", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index 8dc4890..66396d6 100644 --- a/uv.lock +++ b/uv.lock @@ -580,6 +580,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/87/1a36166f91906e47f430f6d4150ec6bfc0001032a363e49fc389021c0609/genai_prices-0.0.66-py3-none-any.whl", hash = "sha256:86b83f107c1cf04bb449a120cd8d4439ceb6843660d9128cde560eb511686d7b", size = 73745, upload-time = "2026-06-09T21:51:13.523Z" }, ] +[[package]] +name = "gherkin-official" +version = "29.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/d8/7a28537efd7638448f7512a0cce011d4e3bf1c7f4794ad4e9c87b3f1e98e/gherkin_official-29.0.0.tar.gz", hash = "sha256:dbea32561158f02280d7579d179b019160d072ce083197625e2f80a6776bb9eb", size = 32303, upload-time = "2024-08-12T09:41:09.595Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/fc/b86c22ad3b18d8324a9d6fe5a3b55403291d2bf7572ba6a16efa5aa88059/gherkin_official-29.0.0-py3-none-any.whl", hash = "sha256:26967b0d537a302119066742669e0e8b663e632769330be675457ae993e1d1bc", size = 37085, upload-time = "2024-08-12T09:41:07.954Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.65.0" @@ -887,6 +896,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/3f/821f2adeb240ffca16a3f89f56aa3733a99574a023e02ed31446bbdf4f61/jinja2_strcase-0.0.2-py2.py3-none-any.whl", hash = "sha256:e3067062f6158cd836ab495f805fc0d1d83cf92049e180509b004db86f6b745c", size = 3933, upload-time = "2020-06-21T16:52:35.979Z" }, ] +[[package]] +name = "json5" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/7d/05c46a96a78147ae3bf99c2f4169ce144a70220b8d6fcd56f6ec368b8ce9/json5-0.15.0.tar.gz", hash = "sha256:7424d1f1eb1d56da6e3d70643f53619862b4ce81440bdb8ecfd6f875e5ba4a71", size = 53278, upload-time = "2026-06-19T20:08:27.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/be/59527c99478aade6bb33a68d72e6e18dd4e6ff6eacfc7d01bdb15bc76912/json5-0.15.0-py3-none-any.whl", hash = "sha256:56636a30c0e8a4665fe2179c0212f32eae3796dea89ea6f649b9436ecdb39618", size = 36570, upload-time = "2026-06-19T20:08:26.748Z" }, +] + +[[package]] +name = "jsonpath-ng" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/32/58/250751940d75c8019659e15482d548a4aa3b6ce122c515102a4bfdac50e3/jsonpath_ng-1.8.0.tar.gz", hash = "sha256:54252968134b5e549ea5b872f1df1168bd7defe1a52fed5a358c194e1943ddc3", size = 74513, upload-time = "2026-02-24T14:42:06.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/99/33c7d78a3fb70d545fd5411ac67a651c81602cc09c9cf0df383733f068c5/jsonpath_ng-1.8.0-py3-none-any.whl", hash = "sha256:b8dde192f8af58d646fc031fac9c99fe4d00326afc4148f1f043c601a8cfe138", size = 67844, upload-time = "2026-02-28T00:53:19.637Z" }, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -941,6 +968,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/81/4b3b994c9649daa3af426c1199a9b4145685eb2d602048b7d7dd79073e18/logfire_api-4.36.0-py3-none-any.whl", hash = "sha256:e2898270969d05b654366b8179576a741065bb22a031078088045ff6ffead0e7", size = 138676, upload-time = "2026-06-09T19:15:59.565Z" }, ] +[[package]] +name = "mako" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/12/b5fa2353e2754cd67fb9f83793fa48ff42c213a5da7e719869d2301f6ab8/mako-1.4.1.tar.gz", hash = "sha256:d7904710b662996425a21627710c4777c45053146942cf8a7aebf757c92b8c27", size = 410165, upload-time = "2026-08-05T06:10:56.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/54/12ed58d458474aaab5c3d180173e745a4fe131bb330370596876d19ff60f/mako-1.4.1-py3-none-any.whl", hash = "sha256:a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617", size = 80010, upload-time = "2026-08-05T06:10:58.248Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1056,7 +1095,7 @@ name = "monorepo" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "reboot" }, + { name = "reboot", extra = ["dev"] }, ] [package.dev-dependencies] @@ -1067,7 +1106,7 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "reboot", specifier = "==1.5.0" }] +requires-dist = [{ name = "reboot", extras = ["dev"], specifier = "==1.6.0" }] [package.metadata.requires-dev] dev = [ @@ -1427,6 +1466,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "parse" +version = "1.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/f2/0b504486c2a5564798607d3860e48ed19c6443d5e9cc3ec61cc6b8b4ef58/parse-1.22.1.tar.gz", hash = "sha256:d3a4740ec3da338e2b258b2d69741b731eadfddca59e24a14bc4ee5fce38c911", size = 36970, upload-time = "2026-05-26T03:44:52.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/c5/7c16e99869e1f422629092cfd23e3b58e461988c3f9c36fd3624bb4142e6/parse-1.22.1-py2.py3-none-any.whl", hash = "sha256:20f0925a46f06602485ac90d751764d0697fd8455aaa97489ba8953a4b66de32", size = 20925, upload-time = "2026-05-26T03:44:51.156Z" }, +] + +[[package]] +name = "parse-type" +version = "0.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parse" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/ea/42ba6ce0abba04ab6e0b997dcb9b528a4661b62af1fe1b0d498120d5ea78/parse_type-0.6.6.tar.gz", hash = "sha256:513a3784104839770d690e04339a8b4d33439fcd5dd99f2e4580f9fc1097bfb2", size = 98012, upload-time = "2025-08-11T22:53:48.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/8d/eef3d8cdccc32abdd91b1286884c99b8c3a6d3b135affcc2a7a0f383bb32/parse_type-0.6.6-py2.py3-none-any.whl", hash = "sha256:3ca79bbe71e170dfccc8ec6c341edfd1c2a0fc1e5cfd18330f93af938de2348c", size = 27085, upload-time = "2025-08-11T22:53:46.396Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -1575,16 +1636,16 @@ wheels = [ [[package]] name = "protobuf" -version = "5.28.3" +version = "5.29.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/6e/e69eb906fddcb38f8530a12f4b410699972ab7ced4e21524ece9d546ac27/protobuf-5.28.3.tar.gz", hash = "sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b", size = 422479, upload-time = "2024-10-23T01:07:26.626Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/d1/e0a911544ca9993e0f17ce6d3cc0932752356c1b0a834397f28e63479344/protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620", size = 424945, upload-time = "2025-01-08T21:38:51.572Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/c5/05163fad52d7c43e124a545f1372d18266db36036377ad29de4271134a6a/protobuf-5.28.3-cp310-abi3-win32.whl", hash = "sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24", size = 419624, upload-time = "2024-10-23T01:07:08.068Z" }, - { url = "https://files.pythonhosted.org/packages/9c/4c/4563ebe001ff30dca9d7ed12e471fa098d9759712980cde1fd03a3a44fb7/protobuf-5.28.3-cp310-abi3-win_amd64.whl", hash = "sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868", size = 431464, upload-time = "2024-10-23T01:07:11.819Z" }, - { url = "https://files.pythonhosted.org/packages/1c/f2/baf397f3dd1d3e4af7e3f5a0382b868d25ac068eefe1ebde05132333436c/protobuf-5.28.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a3f6857551e53ce35e60b403b8a27b0295f7d6eb63d10484f12bc6879c715687", size = 414743, upload-time = "2024-10-23T01:07:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/85/50/cd61a358ba1601f40e7d38bcfba22e053f40ef2c50d55b55926aecc8fec7/protobuf-5.28.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:3fa2de6b8b29d12c61911505d893afe7320ce7ccba4df913e2971461fa36d584", size = 316511, upload-time = "2024-10-23T01:07:14.51Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ae/3257b09328c0b4e59535e497b0c7537d4954038bdd53a2f0d2f49d15a7c4/protobuf-5.28.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135", size = 316624, upload-time = "2024-10-23T01:07:16.192Z" }, - { url = "https://files.pythonhosted.org/packages/ad/c3/2377c159e28ea89a91cf1ca223f827ae8deccb2c9c401e5ca233cd73002f/protobuf-5.28.3-py3-none-any.whl", hash = "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed", size = 169511, upload-time = "2024-10-23T01:07:24.738Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7a/1e38f3cafa022f477ca0f57a1f49962f21ad25850c3ca0acd3b9d0091518/protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888", size = 422708, upload-time = "2025-01-08T21:38:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/aae8e10512b83de633f2646506a6d835b151edf4b30d18d73afd01447253/protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a", size = 434508, upload-time = "2025-01-08T21:38:35.489Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/3eaedc2ba17a088961d0e3bd396eac764450f431621b58a04ce898acd126/protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e", size = 417825, upload-time = "2025-01-08T21:38:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7c467744d23c3979ce250397e26d8ad8eeb2bea7b18ca12ad58313c1b8d5/protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84", size = 319573, upload-time = "2025-01-08T21:38:37.896Z" }, + { url = "https://files.pythonhosted.org/packages/a8/45/2ebbde52ad2be18d3675b6bee50e68cd73c9e0654de77d595540b5129df8/protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f", size = 319672, upload-time = "2025-01-08T21:38:40.204Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b2/ab07b09e0f6d143dfb839693aa05765257bceaa13d03bf1a696b78323e7a/protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f", size = 172550, upload-time = "2025-01-08T21:38:50.439Z" }, ] [[package]] @@ -1842,7 +1903,7 @@ nodejs = [ [[package]] name = "pytest" -version = "9.0.3" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1853,9 +1914,27 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-bdd" +version = "8.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gherkin-official" }, + { name = "mako" }, + { name = "packaging" }, + { name = "parse" }, + { name = "parse-type" }, + { name = "pytest" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/2f/14c2e55372a5718a93b56aea48cd6ccc15d2d245364e516cd7b19bbd07ad/pytest_bdd-8.1.0.tar.gz", hash = "sha256:ef0896c5cd58816dc49810e8ff1d632f4a12019fb3e49959b2d349ffc1c9bfb5", size = 56147, upload-time = "2024-12-05T21:45:58.83Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7d/1461076b0cc9a9e6fa8b51b9dea2677182ba8bc248d99d95ca321f2c666f/pytest_bdd-8.1.0-py3-none-any.whl", hash = "sha256:2124051e71a05ad7db15296e39013593f72ebf96796e1b023a40e5453c47e5fb", size = 49149, upload-time = "2024-12-05T21:45:56.184Z" }, ] [[package]] @@ -1968,7 +2047,7 @@ wheels = [ [[package]] name = "reboot" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, @@ -2020,9 +2099,18 @@ dependencies = [ { name = "websockets" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/c9/deadaea6b1ccc89536df04b48b7e2e01567092ef7a46bae4e9561c92a041/reboot-1.5.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:805edf950918cf1f97643cd004c2ac03abca9ab1887bc2c859631c8eb4359518", size = 21709840, upload-time = "2026-09-01T23:22:05.728Z" }, - { url = "https://files.pythonhosted.org/packages/7b/78/f1b4c99d43957a078208cb57a128ff961083c9141d8dfd0d2b3f5128aed5/reboot-1.5.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:18ed834274c1442a38b7375d49a0de2c2a8343f09e727827217ed926bc4ced46", size = 25395084, upload-time = "2026-09-01T23:05:32.205Z" }, - { url = "https://files.pythonhosted.org/packages/ff/fe/cba921f1ab9c91d03ec71e338a4bc3faa672b0768641641ee6186e4f26ba/reboot-1.5.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:ac47a95c8f88b6b69a5983bfac5f118d031f102ec707ff9e48aea43e8cf73138", size = 25523127, upload-time = "2026-09-01T23:04:00.691Z" }, + { url = "https://files.pythonhosted.org/packages/61/53/f879431323eeb89fd1cd1f3e234f0701b8fbc4610955c995e69ba5c455c6/reboot-1.6.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:96ceaace42f82a23a7a6d116667406aa2661b7f3e41a530b42f5e8e573c21086", size = 21983662, upload-time = "2026-09-16T11:02:28.184Z" }, + { url = "https://files.pythonhosted.org/packages/f4/00/67cd65203fd0479b299143345ab7d06c9d70ef924ed8738e34d7f224089f/reboot-1.6.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:455128ee678386745843c41dae0f7e2774489d7ea9461cc8b44462bb58c2b14d", size = 25655454, upload-time = "2026-09-16T10:50:00.927Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f7/6d56b46c00980bb94e577edeb6f2a5813d5fe466fa54b49a8323dcc69b92/reboot-1.6.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:c658556cfed4c53f413c9f675a8dda259e0076311f74dc8d04c480b55e9aa069", size = 25780286, upload-time = "2026-09-16T10:47:53.447Z" }, +] + +[package.optional-dependencies] +dev = [ + { name = "gherkin-official" }, + { name = "json5" }, + { name = "jsonpath-ng" }, + { name = "pytest" }, + { name = "pytest-bdd" }, ] [[package]]