From 1248772f605b75793abb70b9b574742a992f498c Mon Sep 17 00:00:00 2001 From: Jonas Linter <{email_address}> Date: Fri, 10 Oct 2025 16:47:19 +0200 Subject: [PATCH] Updateinsert customers --- .coverage | Bin 53248 -> 53248 bytes pyproject.toml | 5 + src/alpine_bits_python/api.py | 74 ++++++++++----- src/alpine_bits_python/util/handshake_util.py | 39 -------- tests/test_api.py | 87 ++++++++++++++++++ uv.lock | 85 ++++++++++++++++- 6 files changed, 228 insertions(+), 62 deletions(-) delete mode 100644 src/alpine_bits_python/util/handshake_util.py diff --git a/.coverage b/.coverage index c0882ba489d43639cf14b5b5b7b82554649c11d5..ea75e840be87c6572b106df4ba3f32b2f9147b23 100644 GIT binary patch literal 53248 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 diff --git a/pyproject.toml b/pyproject.toml index 2ab184f..9552102 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,3 +147,8 @@ select = [ "UP032", # Use f-string instead of `format` call "W", # pycodestyle ] + +[dependency-groups] +dev = [ + "pytest-cov>=7.0.0", +] diff --git a/src/alpine_bits_python/api.py b/src/alpine_bits_python/api.py index fac7e87..422e26b 100644 --- a/src/alpine_bits_python/api.py +++ b/src/alpine_bits_python/api.py @@ -15,6 +15,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse, Response from fastapi.security import HTTPBasic, HTTPBasicCredentials from slowapi.errors import RateLimitExceeded +from sqlalchemy import select from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from alpine_bits_python.schemas import ReservationData @@ -393,28 +394,57 @@ async def process_wix_form_submission(request: Request, data: dict[str, Any], db # use database session - # Save all relevant data to DB (including new fields) - db_customer = DBCustomer( - given_name=first_name, - surname=last_name, - contact_id=contact_id, - name_prefix=name_prefix, - email_address=email, - phone=phone_number, - email_newsletter=email_newsletter, - address_line=address_line, - city_name=city_name, - postal_code=postal_code, - country_code=country_code, - gender=gender, - birth_date=birth_date, - language=language, - address_catalog=False, - name_title=None, - ) - db.add(db_customer) - await db.flush() # This assigns db_customer.id without committing - # await db.refresh(db_customer) + # Check if customer with this contact_id already exists + existing_customer = None + if contact_id: + result = await db.execute( + select(DBCustomer).where(DBCustomer.contact_id == contact_id) + ) + existing_customer = result.scalar_one_or_none() + + if existing_customer: + # Update existing customer with new information + _LOGGER.info("Updating existing customer with contact_id: %s", contact_id) + existing_customer.given_name = first_name + existing_customer.surname = last_name + existing_customer.name_prefix = name_prefix + existing_customer.email_address = email + existing_customer.phone = phone_number + existing_customer.email_newsletter = email_newsletter + existing_customer.address_line = address_line + existing_customer.city_name = city_name + existing_customer.postal_code = postal_code + existing_customer.country_code = country_code + existing_customer.gender = gender + existing_customer.birth_date = birth_date + existing_customer.language = language + existing_customer.address_catalog = False + existing_customer.name_title = None + db_customer = existing_customer + await db.flush() + else: + # Create new customer + _LOGGER.info("Creating new customer with contact_id: %s", contact_id) + db_customer = DBCustomer( + given_name=first_name, + surname=last_name, + contact_id=contact_id, + name_prefix=name_prefix, + email_address=email, + phone=phone_number, + email_newsletter=email_newsletter, + address_line=address_line, + city_name=city_name, + postal_code=postal_code, + country_code=country_code, + gender=gender, + birth_date=birth_date, + language=language, + address_catalog=False, + name_title=None, + ) + db.add(db_customer) + await db.flush() # This assigns db_customer.id without committing # Determine hotel_code and hotel_name # Priority: 1) Form field, 2) Configuration default, 3) Hardcoded fallback diff --git a/src/alpine_bits_python/util/handshake_util.py b/src/alpine_bits_python/util/handshake_util.py deleted file mode 100644 index f6e9e17..0000000 --- a/src/alpine_bits_python/util/handshake_util.py +++ /dev/null @@ -1,39 +0,0 @@ -from xsdata_pydantic.bindings import XmlParser - -from alpine_bits_python.generated.alpinebits import OtaPingRs - - -def main(): - # test parsing a ping request sample - - path = ( - "AlpineBits-HotelData-2024-10/files/samples/Handshake/Handshake-OTA_PingRS.xml" - ) - - with open(path, encoding="utf-8") as f: - xml = f.read() - - # Parse the XML into the request object - - # Test parsing back - - parser = XmlParser() - - parsed_result = parser.from_string(xml, OtaPingRs) - - - parsed_result.warnings.warning[0] - - - - - # save json in echo_data to file with indents - output_path = "echo_data_response.json" - with open(output_path, "w", encoding="utf-8") as out_f: - import json - - json.dump(json.loads(parsed_result.echo_data), out_f, indent=4) - - -if __name__ == "__main__": - main() diff --git a/tests/test_api.py b/tests/test_api.py index d150aa7..1fc75dd 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -263,6 +263,93 @@ class TestWixWebhookEndpoint: data = response.json() assert data["status"] == "success" + def test_wix_webhook_updates_existing_customer(self, client): + """Test that same contact_id updates customer instead of duplicate.""" + # First submission + first_submission = { + "data": { + "submissionId": "test-submission-001", + "submissionTime": "2025-10-07T05:48:41.855Z", + "contact": { + "name": {"first": "John", "last": "Doe"}, + "email": "john.doe@example.com", + "phones": [{"e164Phone": "+1234567890"}], + "locale": "en-US", + "contactId": "fixed-contact-id-123", + }, + "field:anrede": "Mr.", + "field:date_picker_a7c8": "2024-12-25", + "field:date_picker_7e65": "2024-12-31", + "field:number_7cf5": "2", + "field:anzahl_kinder": "0", + } + } + + response = client.post("/api/webhook/wix-form", json=first_submission) + assert response.status_code == 200 + + # Second submission with same contact_id but different data + second_submission = { + "data": { + "submissionId": "test-submission-002", + "submissionTime": "2025-10-08T10:30:00.000Z", + "contact": { + "name": {"first": "John", "last": "Smith"}, # Changed last name + "email": "john.smith@example.com", # Changed email + "phones": [{"e164Phone": "+9876543210"}], # Changed phone + "locale": "de-DE", # Changed locale + "contactId": "fixed-contact-id-123", # Same contact_id + }, + "field:anrede": "Dr.", # Changed prefix + "field:date_picker_a7c8": "2025-01-10", + "field:date_picker_7e65": "2025-01-15", + "field:number_7cf5": "4", + "field:anzahl_kinder": "2", + "field:alter_kind_1": "5", + "field:alter_kind_2": "10", + } + } + + response = client.post("/api/webhook/wix-form", json=second_submission) + assert response.status_code == 200 + + # Verify only one customer exists with updated information + async def check_db(): + from sqlalchemy import select # noqa: PLC0415 + + engine = client.app.state.engine + async_session = async_sessionmaker(engine, expire_on_commit=False) + async with async_session() as session: + + # Check only one customer exists + result = await session.execute(select(Customer)) + customers = result.scalars().all() + assert len(customers) == 1, "Should have exactly one customer" + + customer = customers[0] + # Verify customer was updated with new information + assert customer.given_name == "John" + assert customer.surname == "Smith", "Last name updated" + assert ( + customer.email_address == "john.smith@example.com" + ), "Email updated" + assert customer.phone == "+9876543210", "Phone updated" + assert customer.name_prefix == "Dr.", "Prefix updated" + assert customer.language == "de", "Language updated" + assert customer.contact_id == "fixed-contact-id-123" + + # Check both reservations were created + result = await session.execute(select(Reservation)) + reservations = result.scalars().all() + expected_reservations = 2 + assert len(reservations) == expected_reservations + # Both reservations should be linked to the same customer + assert all(r.customer_id == customer.id for r in reservations) + + import asyncio # noqa: PLC0415 + + asyncio.run(check_db()) + class TestGenericWebhookEndpoint: """Test generic webhook endpoint.""" diff --git a/uv.lock b/uv.lock index d63a246..5fca42f 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.13" [[package]] @@ -39,6 +39,11 @@ dependencies = [ { name = "xsdata-pydantic", extra = ["cli", "lxml", "soap"] }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest-cov" }, +] + [package.metadata] requires-dist = [ { name = "aiosqlite", specifier = ">=0.21.0" }, @@ -61,6 +66,9 @@ requires-dist = [ { name = "xsdata-pydantic", extras = ["cli", "lxml", "soap"], specifier = ">=24.5" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest-cov", specifier = ">=7.0.0" }] + [[package]] name = "annotated-types" version = "0.7.0" @@ -196,6 +204,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + [[package]] name = "deprecated" version = "1.2.18" @@ -602,6 +671,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + [[package]] name = "python-dotenv" version = "1.1.1"