============================= test session starts ============================== platform linux -- Python 3.13.5, pytest-8.4.2, pluggy-1.6.0 -- /workspace/gitea-atd/Ty_Tech/SlabUploader/backend/.venv/bin/python3 cachedir: .pytest_cache rootdir: /workspace/gitea-atd/Ty_Tech/SlabUploader/backend configfile: pyproject.toml plugins: anyio-4.15.1, respx-0.23.1 collecting ... collected 39 items tests/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_username PASSED [ 2%] tests/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_password PASSED [ 5%] tests/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_empty_string PASSED [ 7%] tests/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_unicode PASSED [ 10%] tests/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_special_chars PASSED [ 12%] tests/unit/test_aes.py::TestAESRoundTrip::test_ciphertext_is_bytes PASSED [ 15%] tests/unit/test_aes.py::TestAESRoundTrip::test_ciphertext_starts_with_nonce PASSED [ 17%] tests/unit/test_aes.py::TestAESRoundTrip::test_ciphertext_different_from_plaintext PASSED [ 20%] tests/unit/test_aes.py::TestAESRoundTrip::test_ciphertext_changes_each_call PASSED [ 23%] tests/unit/test_aes.py::TestFreshNonce::test_nonce_differs_across_encryptions PASSED [ 25%] tests/unit/test_aes.py::TestFreshNonce::test_multiple_encryptions_all_unique_nonces PASSED [ 28%] tests/unit/test_aes.py::TestTamperedCiphertext::test_tampered_body_raises_authentication_error PASSED [ 30%] tests/unit/test_aes.py::TestTamperedCiphertext::test_truncated_ciphertext_raises_error PASSED [ 33%] tests/unit/test_aes.py::TestTamperedCiphertext::test_wrong_key_raises_error PASSED [ 35%] tests/unit/test_aes.py::TestMissingKey::test_missing_key_refuses_startup PASSED [ 38%] tests/unit/test_aes.py::TestMissingKey::test_too_short_key_refuses_startup PASSED [ 41%] tests/unit/test_aes.py::TestMissingKey::test_get_aes_key_returns_bytes PASSED [ 43%] tests/unit/test_aes.py::TestNoPlaintextFallback::test_decrypt_returns_decrypted_value PASSED [ 46%] tests/contract/test_health.py::TestHealthEndpoint::test_returns_200 PASSED [ 48%] tests/contract/test_health.py::TestHealthEndpoint::test_returns_ok_status PASSED [ 51%] tests/contract/test_health.py::TestHealthEndpoint::test_woo_reachable_false_when_credentials_not_configured PASSED [ 53%] tests/contract/test_health.py::TestHealthEndpoint::test_response_shape PASSED [ 56%] tests/contract/test_health.py::TestHealthEndpoint::test_with_test_client_lifespan PASSED [ 58%] tests/contract/test_slabs.py::TestCreateSlab::test_returns_201 FAILED [ 61%] tests/contract/test_slabs.py::TestCreateSlab::test_returns_status_calibrated FAILED [ 64%] tests/contract/test_slabs.py::TestCreateSlab::test_returns_server_rev FAILED [ 66%] tests/contract/test_slabs.py::TestCreateSlab::test_response_shape FAILED [ 69%] tests/contract/test_slabs.py::TestCreateSlab::test_multiple_photos FAILED [ 71%] tests/contract/test_slabs.py::TestCreateSlab::test_missing_data_field_returns_400 FAILED [ 74%] tests/contract/test_slabs.py::TestCreateSlab::test_duplicate_sku_returns_409 FAILED [ 76%] tests/contract/test_slabs.py::TestCreateSlab::test_with_test_client_lifespan FAILED [ 79%] tests/contract/test_woo_base_url.py::TestWooBaseUrlValidation::test_refuses_http_url PASSED [ 82%] tests/contract/test_woo_base_url.py::TestWooBaseUrlValidation::test_refuses_missing_url PASSED [ 84%] tests/contract/test_woo_base_url.py::TestWooBaseUrlValidation::test_accepts_https_url PASSED [ 87%] tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_get_settings_returns_woo_base_url PASSED [ 89%] tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_put_settings_rejects_woo_base_url PASSED [ 92%] tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_put_settings_allows_other_fields PASSED [ 94%] tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_get_settings_never_returns_app_password PASSED [ 97%] tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_put_settings_rejects_consumer_keys PASSED [100%] =================================== FAILURES =================================== _______________________ TestCreateSlab.test_returns_201 ________________________ self = def test_returns_201(self) -> None: """D-1: POST /api/v1/slabs returns 201 on valid multipart upload.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) slab_id = str(uuid.uuid4()) data_json = json.dumps({ "id": slab_id, "sku": "BW-0042", "length_in": 96.0, "thickness_in": 1.5, "sqft": 0.64, "bdft": 0.96, "width_min_in": 14.0, "width_max_in": 22.0, "width_avg_in": 18.2, "species_id": 42, "wood_category_ids": [15, 23], "edge_type_term_id": 8, "figure_term_ids": [3], "grade_term_ids": [7], "thickness_term_id": 12, "moisture_term_id": 5, "fig_tag_ids": [99], "feat_tag_ids": [42], "price_per_bdft": 12.50, "price": 12.00, "price_source": "recommendation", "title": None, "short_title": None, "description": None, "content_source": None, "client_rev": 1, }) with TestClient(app) as client: > response = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) tests/contract/test_slabs.py:65: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ .venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post return super().post( .venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post return self.request( .venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request return super().request( .venv/lib/python3.13/site-packages/httpx/_client.py:825: in request return self.send(request, auth=auth, follow_redirects=follow_redirects) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:914: in send response = self._send_handling_auth( .venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth response = self._send_handling_redirects( .venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects response = self._send_single_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request response = transport.handle_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request raise exc .venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request portal.call(self.app, scope, receive, send) .venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call return cast(T_Retval, self.start_task_soon(func, *args).result()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:456: in result return self.__get_result() ^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result raise self._exception .venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func retval = await retval_or_awaitable ^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ await super().__call__(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ raise exc .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ await self.app(scope, receive, _send) .venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ await self.app(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app await route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle await self.original_router.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle await included_router._handle_selected(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected await original_route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle await app(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app await wrap_app_handling_exceptions(app, request)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app response = await f(request) ^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app raw_response = await run_endpoint_function( .venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function return await dependant.call(**values) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ app/api/slabs.py:263: in create_slab form = await request.form() ^^^^^^^^^^^^^^^^^^^^ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = async def _get_form( self, *, max_files: int | float = 1000, max_fields: int | float = 1000, max_part_size: int = 1024 * 1024, ) -> FormData: if self._form is None: # pragma: no branch > assert parse_options_header is not None, ( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ "The `python-multipart` library must be installed to use form parsing." ) E AssertionError: The `python-multipart` library must be installed to use form parsing. .venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError ------------------------------ Captured log call ------------------------------- INFO root:main.py:24 SlabUploader starting up INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: INFO app.db.engine:engine.py:69 Database initialized INFO root:main.py:27 SlabUploader shutting down ________________ TestCreateSlab.test_returns_status_calibrated _________________ self = def test_returns_status_calibrated(self) -> None: """D-1: Response contains status: calibrated.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) slab_id = str(uuid.uuid4()) data_json = json.dumps({ "id": slab_id, "sku": "BW-0099", "length_in": 96.0, "thickness_in": 1.5, "sqft": 0.64, "bdft": 0.96, "width_min_in": 14.0, "width_max_in": 22.0, "width_avg_in": 18.2, "species_id": 42, "wood_category_ids": [15, 23], "edge_type_term_id": 8, "figure_term_ids": [3], "grade_term_ids": [7], "thickness_term_id": 12, "moisture_term_id": 5, "fig_tag_ids": [99], "feat_tag_ids": [42], "price_per_bdft": 12.50, "price": 12.00, "price_source": "recommendation", "title": None, "short_title": None, "description": None, "content_source": None, "client_rev": 1, }) with TestClient(app) as client: > response = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) tests/contract/test_slabs.py:122: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ .venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post return super().post( .venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post return self.request( .venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request return super().request( .venv/lib/python3.13/site-packages/httpx/_client.py:825: in request return self.send(request, auth=auth, follow_redirects=follow_redirects) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:914: in send response = self._send_handling_auth( .venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth response = self._send_handling_redirects( .venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects response = self._send_single_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request response = transport.handle_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request raise exc .venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request portal.call(self.app, scope, receive, send) .venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call return cast(T_Retval, self.start_task_soon(func, *args).result()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:456: in result return self.__get_result() ^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result raise self._exception .venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func retval = await retval_or_awaitable ^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ await super().__call__(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ raise exc .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ await self.app(scope, receive, _send) .venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ await self.app(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app await route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle await self.original_router.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle await included_router._handle_selected(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected await original_route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle await app(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app await wrap_app_handling_exceptions(app, request)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app response = await f(request) ^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app raw_response = await run_endpoint_function( .venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function return await dependant.call(**values) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ app/api/slabs.py:263: in create_slab form = await request.form() ^^^^^^^^^^^^^^^^^^^^ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = async def _get_form( self, *, max_files: int | float = 1000, max_fields: int | float = 1000, max_part_size: int = 1024 * 1024, ) -> FormData: if self._form is None: # pragma: no branch > assert parse_options_header is not None, ( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ "The `python-multipart` library must be installed to use form parsing." ) E AssertionError: The `python-multipart` library must be installed to use form parsing. .venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError ------------------------------ Captured log call ------------------------------- INFO root:main.py:24 SlabUploader starting up INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: INFO app.db.engine:engine.py:69 Database initialized INFO root:main.py:27 SlabUploader shutting down ____________________ TestCreateSlab.test_returns_server_rev ____________________ self = def test_returns_server_rev(self) -> None: """D-1: Response contains server_rev.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) slab_id = str(uuid.uuid4()) data_json = json.dumps({ "id": slab_id, "sku": "BW-0077", "length_in": 96.0, "thickness_in": 1.5, "sqft": 0.64, "bdft": 0.96, "width_min_in": 14.0, "width_max_in": 22.0, "width_avg_in": 18.2, "species_id": 42, "wood_category_ids": [15, 23], "edge_type_term_id": 8, "figure_term_ids": [3], "grade_term_ids": [7], "thickness_term_id": 12, "moisture_term_id": 5, "fig_tag_ids": [99], "feat_tag_ids": [42], "price_per_bdft": 12.50, "price": 12.00, "price_source": "recommendation", "title": None, "short_title": None, "description": None, "content_source": None, "client_rev": 1, }) with TestClient(app) as client: > response = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) tests/contract/test_slabs.py:181: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ .venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post return super().post( .venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post return self.request( .venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request return super().request( .venv/lib/python3.13/site-packages/httpx/_client.py:825: in request return self.send(request, auth=auth, follow_redirects=follow_redirects) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:914: in send response = self._send_handling_auth( .venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth response = self._send_handling_redirects( .venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects response = self._send_single_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request response = transport.handle_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request raise exc .venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request portal.call(self.app, scope, receive, send) .venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call return cast(T_Retval, self.start_task_soon(func, *args).result()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:456: in result return self.__get_result() ^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result raise self._exception .venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func retval = await retval_or_awaitable ^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ await super().__call__(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ raise exc .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ await self.app(scope, receive, _send) .venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ await self.app(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app await route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle await self.original_router.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle await included_router._handle_selected(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected await original_route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle await app(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app await wrap_app_handling_exceptions(app, request)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app response = await f(request) ^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app raw_response = await run_endpoint_function( .venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function return await dependant.call(**values) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ app/api/slabs.py:263: in create_slab form = await request.form() ^^^^^^^^^^^^^^^^^^^^ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = async def _get_form( self, *, max_files: int | float = 1000, max_fields: int | float = 1000, max_part_size: int = 1024 * 1024, ) -> FormData: if self._form is None: # pragma: no branch > assert parse_options_header is not None, ( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ "The `python-multipart` library must be installed to use form parsing." ) E AssertionError: The `python-multipart` library must be installed to use form parsing. .venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError ------------------------------ Captured log call ------------------------------- INFO root:main.py:24 SlabUploader starting up INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: INFO app.db.engine:engine.py:69 Database initialized INFO root:main.py:27 SlabUploader shutting down ______________________ TestCreateSlab.test_response_shape ______________________ self = def test_response_shape(self) -> None: """D-1: Response contains required fields.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) slab_id = str(uuid.uuid4()) data_json = json.dumps({ "id": slab_id, "sku": "BW-0033", "length_in": 96.0, "thickness_in": 1.5, "sqft": 0.64, "bdft": 0.96, "width_min_in": 14.0, "width_max_in": 22.0, "width_avg_in": 18.2, "species_id": 42, "wood_category_ids": [15, 23], "edge_type_term_id": 8, "figure_term_ids": [3], "grade_term_ids": [7], "thickness_term_id": 12, "moisture_term_id": 5, "fig_tag_ids": [99], "feat_tag_ids": [42], "price_per_bdft": 12.50, "price": 12.00, "price_source": "recommendation", "title": None, "short_title": None, "description": None, "content_source": None, "client_rev": 1, }) with TestClient(app) as client: > response = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) tests/contract/test_slabs.py:241: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ .venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post return super().post( .venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post return self.request( .venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request return super().request( .venv/lib/python3.13/site-packages/httpx/_client.py:825: in request return self.send(request, auth=auth, follow_redirects=follow_redirects) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:914: in send response = self._send_handling_auth( .venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth response = self._send_handling_redirects( .venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects response = self._send_single_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request response = transport.handle_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request raise exc .venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request portal.call(self.app, scope, receive, send) .venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call return cast(T_Retval, self.start_task_soon(func, *args).result()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:456: in result return self.__get_result() ^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result raise self._exception .venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func retval = await retval_or_awaitable ^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ await super().__call__(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ raise exc .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ await self.app(scope, receive, _send) .venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ await self.app(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app await route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle await self.original_router.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle await included_router._handle_selected(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected await original_route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle await app(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app await wrap_app_handling_exceptions(app, request)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app response = await f(request) ^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app raw_response = await run_endpoint_function( .venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function return await dependant.call(**values) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ app/api/slabs.py:263: in create_slab form = await request.form() ^^^^^^^^^^^^^^^^^^^^ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = async def _get_form( self, *, max_files: int | float = 1000, max_fields: int | float = 1000, max_part_size: int = 1024 * 1024, ) -> FormData: if self._form is None: # pragma: no branch > assert parse_options_header is not None, ( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ "The `python-multipart` library must be installed to use form parsing." ) E AssertionError: The `python-multipart` library must be installed to use form parsing. .venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError ------------------------------ Captured log call ------------------------------- INFO root:main.py:24 SlabUploader starting up INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: INFO app.db.engine:engine.py:69 Database initialized INFO root:main.py:27 SlabUploader shutting down _____________________ TestCreateSlab.test_multiple_photos ______________________ self = def test_multiple_photos(self) -> None: """D-1: Multiple files[] and meta[] pairs are accepted.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) slab_id = str(uuid.uuid4()) data_json = json.dumps({ "id": slab_id, "sku": "BW-0055", "length_in": 96.0, "thickness_in": 1.5, "sqft": 0.64, "bdft": 0.96, "width_min_in": 14.0, "width_max_in": 22.0, "width_avg_in": 18.2, "species_id": 42, "wood_category_ids": [15, 23], "edge_type_term_id": 8, "figure_term_ids": [3], "grade_term_ids": [7], "thickness_term_id": 12, "moisture_term_id": 5, "fig_tag_ids": [99], "feat_tag_ids": [42], "price_per_bdft": 12.50, "price": 12.00, "price_source": "recommendation", "title": None, "short_title": None, "description": None, "content_source": None, "client_rev": 1, }) with TestClient(app) as client: > response = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ("files[]", ("extra1.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) tests/contract/test_slabs.py:335: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ .venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post return super().post( .venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post return self.request( .venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request return super().request( .venv/lib/python3.13/site-packages/httpx/_client.py:825: in request return self.send(request, auth=auth, follow_redirects=follow_redirects) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:914: in send response = self._send_handling_auth( .venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth response = self._send_handling_redirects( .venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects response = self._send_single_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request response = transport.handle_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request raise exc .venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request portal.call(self.app, scope, receive, send) .venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call return cast(T_Retval, self.start_task_soon(func, *args).result()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:456: in result return self.__get_result() ^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result raise self._exception .venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func retval = await retval_or_awaitable ^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ await super().__call__(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ raise exc .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ await self.app(scope, receive, _send) .venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ await self.app(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app await route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle await self.original_router.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle await included_router._handle_selected(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected await original_route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle await app(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app await wrap_app_handling_exceptions(app, request)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app response = await f(request) ^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app raw_response = await run_endpoint_function( .venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function return await dependant.call(**values) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ app/api/slabs.py:263: in create_slab form = await request.form() ^^^^^^^^^^^^^^^^^^^^ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = async def _get_form( self, *, max_files: int | float = 1000, max_fields: int | float = 1000, max_part_size: int = 1024 * 1024, ) -> FormData: if self._form is None: # pragma: no branch > assert parse_options_header is not None, ( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ "The `python-multipart` library must be installed to use form parsing." ) E AssertionError: The `python-multipart` library must be installed to use form parsing. .venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError ------------------------------ Captured log call ------------------------------- INFO root:main.py:24 SlabUploader starting up INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: INFO app.db.engine:engine.py:69 Database initialized INFO root:main.py:27 SlabUploader shutting down ______________ TestCreateSlab.test_missing_data_field_returns_400 ______________ self = def test_missing_data_field_returns_400(self) -> None: """D-1: Missing data field returns 400.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) with TestClient(app) as client: > response = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) tests/contract/test_slabs.py:367: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ .venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post return super().post( .venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post return self.request( .venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request return super().request( .venv/lib/python3.13/site-packages/httpx/_client.py:825: in request return self.send(request, auth=auth, follow_redirects=follow_redirects) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:914: in send response = self._send_handling_auth( .venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth response = self._send_handling_redirects( .venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects response = self._send_single_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request response = transport.handle_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request raise exc .venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request portal.call(self.app, scope, receive, send) .venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call return cast(T_Retval, self.start_task_soon(func, *args).result()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:456: in result return self.__get_result() ^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result raise self._exception .venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func retval = await retval_or_awaitable ^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ await super().__call__(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ raise exc .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ await self.app(scope, receive, _send) .venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ await self.app(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app await route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle await self.original_router.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle await included_router._handle_selected(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected await original_route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle await app(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app await wrap_app_handling_exceptions(app, request)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app response = await f(request) ^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app raw_response = await run_endpoint_function( .venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function return await dependant.call(**values) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ app/api/slabs.py:263: in create_slab form = await request.form() ^^^^^^^^^^^^^^^^^^^^ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = async def _get_form( self, *, max_files: int | float = 1000, max_fields: int | float = 1000, max_part_size: int = 1024 * 1024, ) -> FormData: if self._form is None: # pragma: no branch > assert parse_options_header is not None, ( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ "The `python-multipart` library must be installed to use form parsing." ) E AssertionError: The `python-multipart` library must be installed to use form parsing. .venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError ------------------------------ Captured log call ------------------------------- INFO root:main.py:24 SlabUploader starting up INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: INFO app.db.engine:engine.py:69 Database initialized INFO root:main.py:27 SlabUploader shutting down ________________ TestCreateSlab.test_duplicate_sku_returns_409 _________________ self = def test_duplicate_sku_returns_409(self) -> None: """D-1: Duplicate SKU returns 409.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) slab_id = str(uuid.uuid4()) sku = "DUPLICATE-TEST" data_json = json.dumps({ "id": slab_id, "sku": sku, "length_in": 96.0, "thickness_in": 1.5, "sqft": 0.64, "bdft": 0.96, "width_min_in": 14.0, "width_max_in": 22.0, "width_avg_in": 18.2, "species_id": 42, "wood_category_ids": [15, 23], "edge_type_term_id": 8, "figure_term_ids": [3], "grade_term_ids": [7], "thickness_term_id": 12, "moisture_term_id": 5, "fig_tag_ids": [99], "feat_tag_ids": [42], "price_per_bdft": 12.50, "price": 12.00, "price_source": "recommendation", "title": None, "short_title": None, "description": None, "content_source": None, "client_rev": 1, }) with TestClient(app) as client: # Create first slab > response1 = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) tests/contract/test_slabs.py:427: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ .venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post return super().post( .venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post return self.request( .venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request return super().request( .venv/lib/python3.13/site-packages/httpx/_client.py:825: in request return self.send(request, auth=auth, follow_redirects=follow_redirects) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:914: in send response = self._send_handling_auth( .venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth response = self._send_handling_redirects( .venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects response = self._send_single_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request response = transport.handle_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request raise exc .venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request portal.call(self.app, scope, receive, send) .venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call return cast(T_Retval, self.start_task_soon(func, *args).result()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:456: in result return self.__get_result() ^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result raise self._exception .venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func retval = await retval_or_awaitable ^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ await super().__call__(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ raise exc .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ await self.app(scope, receive, _send) .venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ await self.app(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app await route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle await self.original_router.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle await included_router._handle_selected(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected await original_route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle await app(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app await wrap_app_handling_exceptions(app, request)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app response = await f(request) ^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app raw_response = await run_endpoint_function( .venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function return await dependant.call(**values) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ app/api/slabs.py:263: in create_slab form = await request.form() ^^^^^^^^^^^^^^^^^^^^ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = async def _get_form( self, *, max_files: int | float = 1000, max_fields: int | float = 1000, max_part_size: int = 1024 * 1024, ) -> FormData: if self._form is None: # pragma: no branch > assert parse_options_header is not None, ( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ "The `python-multipart` library must be installed to use form parsing." ) E AssertionError: The `python-multipart` library must be installed to use form parsing. .venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError ------------------------------ Captured log call ------------------------------- INFO root:main.py:24 SlabUploader starting up INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: INFO app.db.engine:engine.py:69 Database initialized INFO root:main.py:27 SlabUploader shutting down ________________ TestCreateSlab.test_with_test_client_lifespan _________________ self = def test_with_test_client_lifespan(self) -> None: """FUND-9: TestClient with app manages lifespan correctly for slabs.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) slab_id = str(uuid.uuid4()) data_json = json.dumps({ "id": slab_id, "sku": "BW-0011", "length_in": 96.0, "thickness_in": 1.5, "sqft": 0.64, "bdft": 0.96, "width_min_in": 14.0, "width_max_in": 22.0, "width_avg_in": 18.2, "species_id": 42, "wood_category_ids": [15, 23], "edge_type_term_id": 8, "figure_term_ids": [3], "grade_term_ids": [7], "thickness_term_id": 12, "moisture_term_id": 5, "fig_tag_ids": [99], "feat_tag_ids": [42], "price_per_bdft": 12.50, "price": 12.00, "price_source": "recommendation", "title": None, "short_title": None, "description": None, "content_source": None, "client_rev": 1, }) with TestClient(app) as client: > response = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) tests/contract/test_slabs.py:503: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ .venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post return super().post( .venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post return self.request( .venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request return super().request( .venv/lib/python3.13/site-packages/httpx/_client.py:825: in request return self.send(request, auth=auth, follow_redirects=follow_redirects) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:914: in send response = self._send_handling_auth( .venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth response = self._send_handling_redirects( .venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects response = self._send_single_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request response = transport.handle_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request raise exc .venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request portal.call(self.app, scope, receive, send) .venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call return cast(T_Retval, self.start_task_soon(func, *args).result()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:456: in result return self.__get_result() ^^^^^^^^^^^^^^^^^^^ /usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result raise self._exception .venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func retval = await retval_or_awaitable ^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ await super().__call__(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ raise exc .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ await self.app(scope, receive, _send) .venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ await self.app(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app await route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle await self.original_router.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle await included_router._handle_selected(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected await original_route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle await app(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app await wrap_app_handling_exceptions(app, request)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app response = await f(request) ^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app raw_response = await run_endpoint_function( .venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function return await dependant.call(**values) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ app/api/slabs.py:263: in create_slab form = await request.form() ^^^^^^^^^^^^^^^^^^^^ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = async def _get_form( self, *, max_files: int | float = 1000, max_fields: int | float = 1000, max_part_size: int = 1024 * 1024, ) -> FormData: if self._form is None: # pragma: no branch > assert parse_options_header is not None, ( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ "The `python-multipart` library must be installed to use form parsing." ) E AssertionError: The `python-multipart` library must be installed to use form parsing. .venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError ------------------------------ Captured log call ------------------------------- INFO root:main.py:24 SlabUploader starting up INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: INFO app.db.engine:engine.py:69 Database initialized INFO root:main.py:27 SlabUploader shutting down =============================== warnings summary =============================== .venv/lib/python3.13/site-packages/fastapi/testclient.py:1 /workspace/gitea-atd/Ty_Tech/SlabUploader/backend/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. from starlette.testclient import TestClient as TestClient # noqa .venv/lib/python3.13/site-packages/starlette/testclient.py:53 /workspace/gitea-atd/Ty_Tech/SlabUploader/backend/.venv/lib/python3.13/site-packages/starlette/testclient.py:53: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead. _PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]] -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html =========================== short test summary info ============================ FAILED tests/contract/test_slabs.py::TestCreateSlab::test_returns_201 - Asser... FAILED tests/contract/test_slabs.py::TestCreateSlab::test_returns_status_calibrated FAILED tests/contract/test_slabs.py::TestCreateSlab::test_returns_server_rev FAILED tests/contract/test_slabs.py::TestCreateSlab::test_response_shape - As... FAILED tests/contract/test_slabs.py::TestCreateSlab::test_multiple_photos - A... FAILED tests/contract/test_slabs.py::TestCreateSlab::test_missing_data_field_returns_400 FAILED tests/contract/test_slabs.py::TestCreateSlab::test_duplicate_sku_returns_409 FAILED tests/contract/test_slabs.py::TestCreateSlab::test_with_test_client_lifespan =================== 8 failed, 31 passed, 2 warnings in 3.52s =================== EXIT:1